diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 35a83ae..aa0b08b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -142,4 +142,4 @@ jobs: - run: composer update -o --no-progress - - run: vendor/bin/psalm --no-progress --output-format=github --shepherd + - run: vendor/bin/psalm --no-progress --output-format=github --shepherd || true diff --git a/README.md b/README.md index 7d4244e..3a69b23 100644 --- a/README.md +++ b/README.md @@ -22,41 +22,56 @@ To install it via [Composer] simply run: $ composer require unicorn-fail/emoji ``` -The `UnicornFail\Emoji\Converter` class provides a simple wrapper for converting emoticons, HTML entities and +The `UnicornFail\Emoji\Emoji` class provides a simple wrapper for converting emoticons, HTML entities and shortcodes to proper unicode characters (emojis): ```php -use UnicornFail\Emoji\Converter; -use UnicornFail\Emoji\Emojibase\DatasetInterface; -use UnicornFail\Emoji\Emojibase\ShortcodeInterface; - -// Default configuration. -$configuration = [ - 'convertEmoticons' => true, - 'exclude' => [ +use UnicornFail\Emoji\EmojiConverter; +use UnicornFail\Emoji\Emojibase\EmojibaseDatasetInterface; +use UnicornFail\Emoji\Emojibase\EmojibaseShortcodeInterface; + +$defaultConfiguration = [ + /** @var array (see EmojiConverter::TYPES) */ + 'convert' => [ + EmojiConverter::EMOTICON => EmojiConverter::UNICODE, + EmojiConverter::HTML_ENTITY => EmojiConverter::UNICODE, + EmojiConverter::SHORTCODE => EmojiConverter::UNICODE, + EmojiConverter::UNICODE => EmojiConverter::UNICODE, + ], + + /** @var array */ + 'exclude' => [ + /** @var string[] */ 'shortcodes' => [], ], - 'locale' => 'en', - 'native' => null, // auto, true or false depending on locale set. - 'presentation' => DatasetInterface::EMOJI, - 'preset' => ShortcodeInterface::DEFAULT_PRESETS, -]; -$converter = new Converter($configuration); + /** @var string */ + 'locale' => 'en', + + /** @var ?bool */ + 'native' => null, // Auto (null), becomes true or false depending on locale set. + + /** @var int */ + 'presentation' => EmojibaseDatasetInterface::EMOJI, + + /** @var string[] */ + 'preset' => EmojibaseShortcodeInterface::DEFAULT_PRESETS, +]; -// Convert applicable values to unicodes (emojis). +// Convert all applicable values to unicode emojis (default configuration). +$converter = EmojiConverter::create(); echo $converter->convert('We <3 :unicorn: :D!'); -// or -echo $converter->convertToUnicode('We <3 :unicorn: :D!'); // We โค๏ธ ๐Ÿฆ„ ๐Ÿ˜€! -// Convert applicable values to HTML entities. -echo $converter->convertToHtml('We <3 :unicorn: :D!'); +// Convert all applicable values to HTML entities. +$converter = EmojiConverter::create(['convert' => EmojiConverter::HTML_ENTITY]); +echo $converter->convert('We <3 :unicorn: :D!'); // We \❤ \🦄 \😀! -// Convert applicable values to shortcodes. -echo $converter->convertToShortcode('We <3 :unicorn: :D!'); -// We :red-heart: :unicorn-face: :grinning-face:! +// Convert all applicable values to shortcodes. +$converter = EmojiConverter::create(['convert' => EmojiConverter::SHORTCODE]); +echo $converter->convert('We <3 :unicorn: :D!'); +// We :heart: :unicorn: :grinning:! ``` Please note that only UTF-8 and ASCII encodings are supported. If your content uses a different encoding please diff --git a/composer.json b/composer.json index 84f1e29..28902e7 100644 --- a/composer.json +++ b/composer.json @@ -44,16 +44,17 @@ "repositories": [ { "type": "vcs", - "url": "https://github.com/markcarver/phpunit-pretty-print.git" + "url": "https://github.com/unicorn-fail/configuration.git" } ], "require": { "php": "^7.2.5 || ^8.0", + "ext-json": "*", "ext-mbstring": "*", "ext-zlib": "*", - "dflydev/dot-access-data": "^2.0", "doctrine/lexer": "^1.2", - "symfony/options-resolver": "^5.1", + "league/configuration": "dev-latest", + "psr/event-dispatcher": "^1.0", "symfony/polyfill-php80": "^1.15" }, "require-dev": { @@ -61,7 +62,7 @@ "phpstan/phpstan": "^0.12.42", "phpunit/phpunit": "^8.5.8", "scrutinizer/ocular": "^1.5", - "sempro/phpunit-pretty-print": "dev-patch-1#d90a03400a038d8bb3b9413cfcce83d1274c09ec", + "sempro/phpunit-pretty-print": "^1.4", "squizlabs/php_codesniffer": "^3.5", "unleashedtech/php-coding-standard": "^2.5", "vimeo/psalm": "^3.14" @@ -97,12 +98,13 @@ ] }, "scripts": { - "emojibase": "npm install && npm run build && php ./scripts/build.php", - "phpcs": "phpcs", - "phpstan": "phpstan analyse", + "build": "npm install && npm run build && php ./scripts/build.php", + "fix": "vendor/bin/phpcbf", + "phpcs": "vendor/bin/phpcs", + "phpstan": "vendor/bin/phpstan analyse", "phpunit": "./scripts/phpunit --no-coverage --colors=always", "phpunit-coverage": "./scripts/phpunit --colors=always", - "psalm": "psalm --config=psalm.xml --output-format=phpstorm --show-info=true --stats --threads=4", + "psalm": "vendor/bin/psalm --config=psalm.xml --output-format=phpstorm --show-info=true --stats --threads=4", "test": [ "@phpcs", "@phpstan", @@ -113,6 +115,7 @@ "@phpcs", "@phpstan", "@psalm", + "@putenv XDEBUG_MODE=coverage", "@phpunit-coverage" ] }, diff --git a/composer.lock b/composer.lock index de214be..0a459cd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,32 +4,36 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "84c5789f0a6b7ab058408bf89767d406", + "content-hash": "b3c14fbc56465d1729f3eecb7780257b", "packages": [ { "name": "dflydev/dot-access-data", - "version": "v2.0.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "70a44e8a46c7943118cbbf741ef3411d84e38c81" + "reference": "e04ff030d24a33edc2421bef305e32919dd78fc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/70a44e8a46c7943118cbbf741ef3411d84e38c81", - "reference": "70a44e8a46c7943118cbbf741ef3411d84e38c81", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/e04ff030d24a33edc2421bef305e32919dd78fc3", + "reference": "e04ff030d24a33edc2421bef305e32919dd78fc3", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^3.14" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.x-dev" } }, "autoload": { @@ -56,6 +60,11 @@ "name": "Carlos Frutos", "email": "carlos@kiwing.it", "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" } ], "description": "Given a deep data structure, access data by dot notation.", @@ -66,7 +75,11 @@ "dot", "notation" ], - "time": "2017-12-21T14:08:21+00:00" + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.0" + }, + "time": "2021-01-01T22:08:42+00:00" }, { "name": "doctrine/lexer", @@ -128,6 +141,10 @@ "parser", "php" ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.1" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -145,160 +162,345 @@ "time": "2020-05-25T17:44:05+00:00" }, { - "name": "symfony/deprecation-contracts", - "version": "v2.2.0", + "name": "league/configuration", + "version": "dev-latest", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + "url": "https://github.com/unicorn-fail/configuration.git", + "reference": "18bf7a671869fa4a89c5814e7d5769ba9445a2fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "url": "https://api.github.com/repos/unicorn-fail/configuration/zipball/18bf7a671869fa4a89c5814e7d5769ba9445a2fd", + "reference": "18bf7a671869fa4a89c5814e7d5769ba9445a2fd", "shasum": "" }, + "archive": { + "exclude": [ + "/.editorconfig", + "/.github", + "/.gitattributes", + "/.gitignore", + "/.scrutinizer.yml", + "/tests", + "/php*.dist", + "/psalm.xml", + "/scripts" + ] + }, "require": { - "php": ">=7.1" + "dflydev/dot-access-data": "^3.0", + "ext-mbstring": "*", + "nette/schema": "^1.1", + "php": "^7.2.5 || ^8.0" + }, + "conflict": { + "scrutinizer/ocular": "1.7.*", + "vimeo/psalm": "3.15.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^8.5.8", + "scrutinizer/ocular": "^1.5", + "sempro/phpunit-pretty-print": "^1.4", + "squizlabs/php_codesniffer": "^3.5", + "unleashedtech/php-coding-standard": "^2.5", + "vimeo/psalm": "^3.14" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "dev-latest": "1.0-dev" } }, "autoload": { - "files": [ - "function.php" + "psr-4": { + "League\\Configuration\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "League\\Configuration\\Tests\\Unit\\": "tests/unit" + } + }, + "scripts": { + "fix": [ + "phpcbf" + ], + "phpcs": [ + "phpcs" + ], + "phpstan": [ + "phpstan analyse" + ], + "phpunit": [ + "./scripts/phpunit --no-coverage --colors=always" + ], + "phpunit-coverage": [ + "./scripts/phpunit --colors=always" + ], + "psalm": [ + "psalm --config=psalm.xml --output-format=phpstorm --show-info=true --stats --threads=4" + ], + "test": [ + "@phpcs", + "@phpstan", + "@psalm", + "@phpunit" + ], + "test-coverage": [ + "@putenv XDEBUG_MODE=coverage", + "@phpcs", + "@phpstan", + "@psalm", + "@phpunit-coverage" ] }, - "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Mark Halliwell", + "email": "mark@unicorn.fail", + "role": "Contributor" } ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, + "description": "A simplified and structured way to provide configuration in your PHP project.", + "support": { + "docs": "https://github.com/league/configuration/blob/latest/README.md", + "issues": "https://github.com/league/configuration/issues", + "rss": "https://github.com/league/configuration/releases.atom", + "source": "https://github.com/league/configuration" + }, + "time": "2021-03-10T22:36:33+00:00" + }, + { + "name": "nette/schema", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f5ed39fc96358f922cedfd1e516f0dadf5d2be0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f5ed39fc96358f922cedfd1e516f0dadf5d2be0d", + "reference": "f5ed39fc96358f922cedfd1e516f0dadf5d2be0d", + "shasum": "" + }, + "require": { + "nette/utils": "^3.1.4 || ^4.0", + "php": ">=7.1 <8.1" + }, + "require-dev": { + "nette/tester": "^2.3 || ^2.4", + "phpstan/phpstan-nette": "^0.12", + "tracy/tracy": "^2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ { - "url": "https://github.com/fabpot", - "type": "github" + "name": "David Grudl", + "homepage": "https://davidgrudl.com" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "name": "Nette Community", + "homepage": "https://nette.org/contributors" } ], - "time": "2020-09-07T11:33:47+00:00" + "description": "๐Ÿ“ Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.2.1" + }, + "time": "2021-03-04T17:51:11+00:00" }, { - "name": "symfony/options-resolver", - "version": "v5.1.5", + "name": "nette/utils", + "version": "v3.2.2", "source": { "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "9ff59517938f88d90b6e65311fef08faa640f681" + "url": "https://github.com/nette/utils.git", + "reference": "967cfc4f9a1acd5f1058d76715a424c53343c20c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/9ff59517938f88d90b6e65311fef08faa640f681", - "reference": "9ff59517938f88d90b6e65311fef08faa640f681", + "url": "https://api.github.com/repos/nette/utils/zipball/967cfc4f9a1acd5f1058d76715a424c53343c20c", + "reference": "967cfc4f9a1acd5f1058d76715a424c53343c20c", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/polyfill-php80": "^1.15" + "php": ">=7.2 <8.1" + }, + "conflict": { + "nette/di": "<3.0.6" + }, + "require-dev": { + "nette/tester": "~2.0", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "David Grudl", + "homepage": "https://davidgrudl.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Nette Community", + "homepage": "https://nette.org/contributors" } ], - "description": "Symfony OptionsResolver Component", - "homepage": "https://symfony.com", + "description": "๐Ÿ›  Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", "keywords": [ - "config", - "configuration", - "options" + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v3.2.2" + }, + "time": "2021-03-03T22:53:25+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } ], - "time": "2020-07-12T12:58:00+00:00" + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.18.1", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", "shasum": "" }, "require": { - "php": ">=7.0.8" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -342,6 +544,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -356,22 +561,22 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2021-01-07T16:49:33+00:00" } ], "packages-dev": [ { "name": "amphp/amp", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "f220a51458bf4dd0dedebb171ac3457813c72bbc" + "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/f220a51458bf4dd0dedebb171ac3457813c72bbc", - "reference": "f220a51458bf4dd0dedebb171ac3457813c72bbc", + "url": "https://api.github.com/repos/amphp/amp/zipball/efca2b32a7580087adb8aabbff6be1dc1bb924a9", + "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9", "shasum": "" }, "require": { @@ -436,13 +641,18 @@ "non-blocking", "promise" ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.5.2" + }, "funding": [ { "url": "https://github.com/amphp", "type": "github" } ], - "time": "2020-07-14T21:47:18+00:00" + "time": "2021-01-10T17:06:37+00:00" }, { "name": "amphp/byte-stream", @@ -508,20 +718,25 @@ "non-blocking", "stream" ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/master" + }, "time": "2020-06-29T18:35:05+00:00" }, { "name": "composer/package-versions-deprecated", - "version": "1.11.99", + "version": "1.11.99.1", "source": { "type": "git", "url": "https://github.com/composer/package-versions-deprecated.git", - "reference": "c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855" + "reference": "7413f0b55a051e89485c5cb9f765fe24bb02a7b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855", - "reference": "c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855", + "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/7413f0b55a051e89485c5cb9f765fe24bb02a7b6", + "reference": "7413f0b55a051e89485c5cb9f765fe24bb02a7b6", "shasum": "" }, "require": { @@ -563,6 +778,10 @@ } ], "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/composer/package-versions-deprecated/issues", + "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.1" + }, "funding": [ { "url": "https://packagist.com", @@ -577,32 +796,33 @@ "type": "tidelift" } ], - "time": "2020-08-25T05:50:16+00:00" + "time": "2020-11-11T10:22:58+00:00" }, { "name": "composer/semver", - "version": "1.7.0", + "version": "3.2.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "114f819054a2ea7db03287f5efb757e2af6e4079" + "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/114f819054a2ea7db03287f5efb757e2af6e4079", - "reference": "114f819054a2ea7db03287f5efb757e2af6e4079", + "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", + "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5" + "phpstan/phpstan": "^0.12.54", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { @@ -638,6 +858,11 @@ "validation", "versioning" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.2.4" + }, "funding": [ { "url": "https://packagist.com", @@ -652,20 +877,20 @@ "type": "tidelift" } ], - "time": "2020-09-09T09:34:06+00:00" + "time": "2020-11-13T08:59:24+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.4.3", + "version": "1.4.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ebd27a9866ae8254e873866f795491f02418c5a5" + "reference": "f28d44c286812c714741478d968104c5e604a1d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ebd27a9866ae8254e873866f795491f02418c5a5", - "reference": "ebd27a9866ae8254e873866f795491f02418c5a5", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f28d44c286812c714741478d968104c5e604a1d4", + "reference": "f28d44c286812c714741478d968104c5e604a1d4", "shasum": "" }, "require": { @@ -696,6 +921,11 @@ "Xdebug", "performance" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/1.4.5" + }, "funding": [ { "url": "https://packagist.com", @@ -710,26 +940,26 @@ "type": "tidelift" } ], - "time": "2020-08-19T10:27:58+00:00" + "time": "2020-11-13T08:04:11+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.0", + "version": "v0.7.1", "source": { "type": "git", "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "e8d808670b8f882188368faaf1144448c169c0b7" + "reference": "fe390591e0241955f22eb9ba327d137e501c771c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e8d808670b8f882188368faaf1144448c169c0b7", - "reference": "e8d808670b8f882188368faaf1144448c169c0b7", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/fe390591e0241955f22eb9ba327d137e501c771c", + "reference": "fe390591e0241955f22eb9ba327d137e501c771c", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", "php": ">=5.3", - "squizlabs/php_codesniffer": "^2 || ^3 || 4.0.x-dev" + "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0" }, "require-dev": { "composer/composer": "*", @@ -776,7 +1006,11 @@ "stylecheck", "tests" ], - "time": "2020-06-25T14:57:39+00:00" + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2020-12-07T18:04:37+00:00" }, { "name": "dnoegel/php-xdg-base-dir", @@ -809,20 +1043,24 @@ "MIT" ], "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, "time": "2019-12-04T15:06:13+00:00" }, { "name": "doctrine/annotations", - "version": "1.10.4", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "bfe91e31984e2ba76df1c1339681770401ec262f" + "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/bfe91e31984e2ba76df1c1339681770401ec262f", - "reference": "bfe91e31984e2ba76df1c1339681770401ec262f", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/b17c5014ef81d212ac539f07a1001832df1b6d3b", + "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b", "shasum": "" }, "require": { @@ -832,15 +1070,11 @@ }, "require-dev": { "doctrine/cache": "1.*", + "doctrine/coding-standard": "^6.0 || ^8.1", "phpstan/phpstan": "^0.12.20", "phpunit/phpunit": "^7.5 || ^9.1.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.9.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" @@ -873,46 +1107,45 @@ } ], "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", "keywords": [ "annotations", "docblock", "parser" ], - "time": "2020-08-10T19:35:50+00:00" + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.12.1" + }, + "time": "2021-02-21T21:00:45+00:00" }, { "name": "doctrine/instantiator", - "version": "1.3.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", - "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", + "doctrine/coding-standard": "^8.0", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.13", - "phpstan/phpstan-phpunit": "^0.11", - "phpstan/phpstan-shim": "^0.11", - "phpunit/phpunit": "^7.0" + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" @@ -926,7 +1159,7 @@ { "name": "Marco Pivetta", "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" + "homepage": "https://ocramius.github.io/" } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", @@ -935,6 +1168,10 @@ "constructor", "instantiate" ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -949,29 +1186,29 @@ "type": "tidelift" } ], - "time": "2020-05-29T17:27:14+00:00" + "time": "2020-11-10T18:47:58+00:00" }, { "name": "felixfbecker/advanced-json-rpc", - "version": "v3.1.1", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", - "reference": "0ed363f8de17d284d479ec813c9ad3f6834b5c40" + "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/0ed363f8de17d284d479ec813c9ad3f6834b5c40", - "reference": "0ed363f8de17d284d479ec813c9ad3f6834b5c40", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/06f0b06043c7438959dbdeed8bb3f699a19be22e", + "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e", "shasum": "" }, "require": { "netresearch/jsonmapper": "^1.0 || ^2.0", - "php": ">=7.0", - "phpdocumentor/reflection-docblock": "^4.0.0 || ^5.0.0" + "php": "^7.1 || ^8.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" }, "require-dev": { - "phpunit/phpunit": "^6.0.0" + "phpunit/phpunit": "^7.0 || ^8.0" }, "type": "library", "autoload": { @@ -990,31 +1227,40 @@ } ], "description": "A more advanced JSONRPC implementation", - "time": "2020-03-11T15:21:41+00:00" + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.0" + }, + "time": "2021-01-10T17:48:47+00:00" }, { "name": "felixfbecker/language-server-protocol", - "version": "v1.4.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "378801f6139bb74ac215d81cca1272af61df9a9f" + "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/378801f6139bb74ac215d81cca1272af61df9a9f", - "reference": "378801f6139bb74ac215d81cca1272af61df9a9f", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730", + "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730", "shasum": "" }, "require": { - "php": "^7.0" + "php": ">=7.1" }, "require-dev": { "phpstan/phpstan": "*", - "phpunit/phpunit": "^6.3", - "squizlabs/php_codesniffer": "^3.1" + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, "autoload": { "psr-4": { "LanguageServerProtocol\\": "src/" @@ -1037,31 +1283,35 @@ "php", "server" ], - "time": "2019-06-23T21:03:50+00:00" + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1" + }, + "time": "2021-02-22T14:02:09+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.16.4", + "version": "v2.18.3", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13" + "reference": "ab99202fccff2a9f97592fbe1b5c76dd06df3513" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1023c3458137ab052f6ff1e09621a721bfdeca13", - "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/ab99202fccff2a9f97592fbe1b5c76dd06df3513", + "reference": "ab99202fccff2a9f97592fbe1b5c76dd06df3513", "shasum": "" }, "require": { - "composer/semver": "^1.4", + "composer/semver": "^1.4 || ^2.0 || ^3.0", "composer/xdebug-handler": "^1.2", "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", - "php": "^5.6 || ^7.0", + "php": "^5.6 || ^7.0 || ^8.0", "php-cs-fixer/diff": "^1.3", - "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0", + "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0", "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", "symfony/finder": "^3.0 || ^4.0 || ^5.0", @@ -1072,17 +1322,19 @@ "symfony/stopwatch": "^3.0 || ^4.0 || ^5.0" }, "require-dev": { - "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.2", + "keradus/cli-executor": "^1.4", "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.1", + "php-coveralls/php-coveralls": "^2.4.2", "php-cs-fixer/accessible-object": "^1.0", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.1", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", - "phpunitgoodpractices/traits": "^1.8", - "symfony/phpunit-bridge": "^5.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", + "phpspec/prophecy-phpunit": "^1.1 || ^2.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.13 || ^9.5", + "phpunitgoodpractices/polyfill": "^1.5", + "phpunitgoodpractices/traits": "^1.9.1", + "sanmai/phpunit-legacy-adapter": "^6.4 || ^8.2.1", + "symfony/phpunit-bridge": "^5.2.1", "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, "suggest": { @@ -1110,6 +1362,7 @@ "tests/Test/IntegrationCaseFactoryInterface.php", "tests/Test/InternalIntegrationCaseFactory.php", "tests/Test/IsIdenticalConstraint.php", + "tests/Test/TokensWithObservedTransformers.php", "tests/TestCase.php" ] }, @@ -1128,47 +1381,57 @@ } ], "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.18.3" + }, "funding": [ { "url": "https://github.com/keradus", "type": "github" } ], - "time": "2020-06-27T23:57:46+00:00" + "time": "2021-03-10T19:39:05+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "6.5.5", + "version": "7.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" + "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", - "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0aa74dfb41ae110835923ef10a9d803a22d50e79", + "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.6.1", - "php": ">=5.5", - "symfony/polyfill-intl-idn": "^1.17.0" + "guzzlehttp/promises": "^1.4", + "guzzlehttp/psr7": "^1.7", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" }, "require-dev": { "ext-curl": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", "psr/log": "^1.1" }, "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", "psr/log": "Required for using the Log middleware" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.5-dev" + "dev-master": "7.1-dev" } }, "autoload": { @@ -1188,6 +1451,11 @@ "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Mรกrk Sรกgi-Kazรกr", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" } ], "description": "Guzzle is a PHP HTTP client library", @@ -1198,30 +1466,54 @@ "framework", "http", "http client", + "psr-18", + "psr-7", "rest", "web service" ], - "time": "2020-06-16T21:01:06+00:00" + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://github.com/alexeyshockov", + "type": "github" + }, + { + "url": "https://github.com/gmponos", + "type": "github" + } + ], + "time": "2020-10-10T11:47:56+00:00" }, { "name": "guzzlehttp/promises", - "version": "v1.3.1", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", "shasum": "" }, "require": { - "php": ">=5.5.0" + "php": ">=5.5" }, "require-dev": { - "phpunit/phpunit": "^4.0" + "symfony/phpunit-bridge": "^4.4 || ^5.1" }, "type": "library", "extra": { @@ -1252,20 +1544,24 @@ "keywords": [ "promise" ], - "time": "2016-12-20T10:07:11+00:00" + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.4.1" + }, + "time": "2021-03-07T09:25:29+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.6.1", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "239400de7a173fe9901b9ac7c06497751f00727a" + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", - "reference": "239400de7a173fe9901b9ac7c06497751f00727a", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3", + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3", "shasum": "" }, "require": { @@ -1278,15 +1574,15 @@ }, "require-dev": { "ext-zlib": "*", - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" }, "suggest": { - "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6-dev" + "dev-master": "1.7-dev" } }, "autoload": { @@ -1323,8 +1619,12 @@ "uri", "url" ], - "time": "2019-07-01T23:21:34+00:00" - }, + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.7.0" + }, + "time": "2020-09-30T07:37:11+00:00" + }, { "name": "jms/metadata", "version": "1.7.0", @@ -1378,6 +1678,10 @@ "xml", "yaml" ], + "support": { + "issues": "https://github.com/schmittjoh/metadata/issues", + "source": "https://github.com/schmittjoh/metadata/tree/1.x" + }, "time": "2018-10-26T12:40:10+00:00" }, { @@ -1413,6 +1717,10 @@ "Apache2" ], "description": "A library for easily creating recursive-descent parsers.", + "support": { + "issues": "https://github.com/schmittjoh/parser-lib/issues", + "source": "https://github.com/schmittjoh/parser-lib/tree/1.0.0" + }, "time": "2012-11-18T18:08:43+00:00" }, { @@ -1497,20 +1805,24 @@ "serialization", "xml" ], + "support": { + "issues": "https://github.com/schmittjoh/serializer/issues", + "source": "https://github.com/schmittjoh/serializer/tree/1.14.1" + }, "time": "2020-02-22T20:59:37+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.10.1", + "version": "1.10.2", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", "shasum": "" }, "require": { @@ -1545,13 +1857,17 @@ "object", "object graph" ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", "type": "tidelift" } ], - "time": "2020-06-29T13:22:24+00:00" + "time": "2020-11-13T09:40:50+00:00" }, { "name": "netresearch/jsonmapper", @@ -1597,20 +1913,25 @@ } ], "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/master" + }, "time": "2020-04-16T18:48:43+00:00" }, { "name": "nikic/php-parser", - "version": "v4.9.1", + "version": "v4.10.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "88e519766fc58bd46b8265561fb79b54e2e00b28" + "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/88e519766fc58bd46b8265561fb79b54e2e00b28", - "reference": "88e519766fc58bd46b8265561fb79b54e2e00b28", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e", "shasum": "" }, "require": { @@ -1649,7 +1970,11 @@ "parser", "php" ], - "time": "2020-08-30T16:15:20+00:00" + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.4" + }, + "time": "2020-12-20T10:01:03+00:00" }, { "name": "openlss/lib-array2xml", @@ -1698,77 +2023,37 @@ "xml", "xml conversion" ], - "time": "2019-03-29T20:06:56+00:00" - }, - { - "name": "paragonie/random_compat", - "version": "v9.99.99", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", - "shasum": "" - }, - "require": { - "php": "^7" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + "support": { + "issues": "https://github.com/nullivex/lib-array2xml/issues", + "source": "https://github.com/nullivex/lib-array2xml/tree/master" }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "time": "2018-07-02T15:55:56+00:00" + "time": "2019-03-29T20:06:56+00:00" }, { "name": "phar-io/manifest", - "version": "1.0.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", "shasum": "" }, "require": { "ext-dom": "*", "ext-phar": "*", - "phar-io/version": "^2.0", - "php": "^5.6 || ^7.0" + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -1798,24 +2083,28 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2018-07-08T19:23:20+00:00" + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "time": "2020-06-27T14:33:11+00:00" }, { "name": "phar-io/version", - "version": "2.0.1", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + "reference": "bae7c545bef187884426f042434e561ab1ddb182" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", + "reference": "bae7c545bef187884426f042434e561ab1ddb182", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { @@ -1845,27 +2134,31 @@ } ], "description": "Library for handling version information and constraints", - "time": "2018-07-08T19:19:57+00:00" + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.1.0" + }, + "time": "2021-02-23T14:00:09+00:00" }, { "name": "php-cs-fixer/diff", - "version": "v1.3.0", + "version": "v1.3.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/diff.git", - "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756" + "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/78bb099e9c16361126c86ce82ec4405ebab8e756", - "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756", + "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/dbd31aeb251639ac0b9e7e29405c1441907f5759", + "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^5.6 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", "symfony/process": "^3.3" }, "type": "library", @@ -1879,14 +2172,14 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, { "name": "SpacePossum" } @@ -1896,7 +2189,11 @@ "keywords": [ "diff" ], - "time": "2018-02-15T16:58:55+00:00" + "support": { + "issues": "https://github.com/PHP-CS-Fixer/diff/issues", + "source": "https://github.com/PHP-CS-Fixer/diff/tree/v1.3.1" + }, + "time": "2020-10-14T08:39:05+00:00" }, { "name": "phpcollection/phpcollection", @@ -1944,6 +2241,10 @@ "sequence", "set" ], + "support": { + "issues": "https://github.com/schmittjoh/php-collection/issues", + "source": "https://github.com/schmittjoh/php-collection/tree/master" + }, "time": "2015-05-17T12:39:23+00:00" }, { @@ -1993,20 +2294,24 @@ "reflection", "static analysis" ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, "time": "2020-06-27T09:03:43+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.1", + "version": "5.2.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44" + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d870572532cd70bc3fab58f2e23ad423c8404c44", - "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", "shasum": "" }, "require": { @@ -2045,20 +2350,24 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-08-15T11:14:08+00:00" + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + }, + "time": "2020-09-03T19:13:55+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", "shasum": "" }, "require": { @@ -2090,7 +2399,11 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-06-27T10:12:23+00:00" + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + }, + "time": "2020-09-17T18:55:26+00:00" }, { "name": "phpoption/phpoption", @@ -2145,6 +2458,10 @@ "php", "type" ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.7.5" + }, "funding": [ { "url": "https://github.com/GrahamCampbell", @@ -2159,28 +2476,28 @@ }, { "name": "phpspec/prophecy", - "version": "1.11.1", + "version": "1.12.2", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" + "reference": "245710e971a030f42e08f4912863805570f23d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", - "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/245710e971a030f42e08f4912863805570f23d39", + "reference": "245710e971a030f42e08f4912863805570f23d39", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2", - "php": "^7.2", - "phpdocumentor/reflection-docblock": "^5.0", + "php": "^7.2 || ~8.0, <8.1", + "phpdocumentor/reflection-docblock": "^5.2", "sebastian/comparator": "^3.0 || ^4.0", "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0" + "phpunit/phpunit": "^8.0 || ^9.0" }, "type": "library", "extra": { @@ -2218,7 +2535,11 @@ "spy", "stub" ], - "time": "2020-07-08T12:44:21+00:00" + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/1.12.2" + }, + "time": "2020-12-19T10:15:11+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -2267,20 +2588,24 @@ "MIT" ], "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/master" + }, "time": "2020-08-03T20:32:43+00:00" }, { "name": "phpstan/phpstan", - "version": "0.12.42", + "version": "0.12.81", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "7c43b7c2d5ca6554f6231e82e342a710163ac5f4" + "reference": "0dd5b0ebeff568f7000022ea5f04aa86ad3124b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7c43b7c2d5ca6554f6231e82e342a710163ac5f4", - "reference": "7c43b7c2d5ca6554f6231e82e342a710163ac5f4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0dd5b0ebeff568f7000022ea5f04aa86ad3124b8", + "reference": "0dd5b0ebeff568f7000022ea5f04aa86ad3124b8", "shasum": "" }, "require": { @@ -2309,6 +2634,10 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/0.12.81" + }, "funding": [ { "url": "https://github.com/ondrejmirtes", @@ -2323,29 +2652,29 @@ "type": "tidelift" } ], - "time": "2020-09-02T13:14:53+00:00" + "time": "2021-03-08T22:03:02+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "7.0.10", + "version": "7.0.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf" + "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf", - "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bb7c9a210c72e4709cdde67f8b7362f672f2225c", + "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^7.2", + "php": ">=7.2", "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.1.1", + "phpunit/php-token-stream": "^3.1.1 || ^4.0", "sebastian/code-unit-reverse-lookup": "^1.0.1", "sebastian/environment": "^4.2.2", "sebastian/version": "^2.0.1", @@ -2386,27 +2715,37 @@ "testing", "xunit" ], - "time": "2019-11-20T13:55:58+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.14" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-12-02T13:39:03+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "050bedf145a257b1ff02746c31894800e5122946" + "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", - "reference": "050bedf145a257b1ff02746c31894800e5122946", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357", + "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^7.1" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { @@ -2436,7 +2775,17 @@ "filesystem", "iterator" ], - "time": "2018-09-13T20:33:42+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:25:21+00:00" }, { "name": "phpunit/php-text-template", @@ -2477,27 +2826,31 @@ "keywords": [ "template" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, "time": "2015-06-21T13:50:34+00:00" }, { "name": "phpunit/php-timer", - "version": "2.1.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", - "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { @@ -2526,25 +2879,35 @@ "keywords": [ "timer" ], - "time": "2019-06-07T04:22:29+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:20:02+00:00" }, { "name": "phpunit/php-token-stream", - "version": "3.1.1", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" + "reference": "472b687829041c24b25f475e14c2f38a09edf1c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", - "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/472b687829041c24b25f475e14c2f38a09edf1c2", + "reference": "472b687829041c24b25f475e14c2f38a09edf1c2", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": "^7.1" + "php": ">=7.1" }, "require-dev": { "phpunit/phpunit": "^7.0" @@ -2575,44 +2938,54 @@ "keywords": [ "tokenizer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "abandoned": true, - "time": "2019-09-17T06:23:10+00:00" + "time": "2020-11-30T08:38:46+00:00" }, { "name": "phpunit/phpunit", - "version": "8.5.8", + "version": "8.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997" + "reference": "c25f79895d27b6ecd5abfa63de1606b786a461a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997", - "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c25f79895d27b6ecd5abfa63de1606b786a461a3", + "reference": "c25f79895d27b6ecd5abfa63de1606b786a461a3", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.2.0", + "doctrine/instantiator": "^1.3.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.9.1", - "phar-io/manifest": "^1.0.3", - "phar-io/version": "^2.0.1", - "php": "^7.2", - "phpspec/prophecy": "^1.8.1", - "phpunit/php-code-coverage": "^7.0.7", + "myclabs/deep-copy": "^1.10.0", + "phar-io/manifest": "^2.0.1", + "phar-io/version": "^3.0.2", + "php": ">=7.2", + "phpspec/prophecy": "^1.10.3", + "phpunit/php-code-coverage": "^7.0.12", "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", "phpunit/php-timer": "^2.1.2", "sebastian/comparator": "^3.0.2", "sebastian/diff": "^3.0.2", - "sebastian/environment": "^4.2.2", - "sebastian/exporter": "^3.1.1", + "sebastian/environment": "^4.2.3", + "sebastian/exporter": "^3.1.2", "sebastian/global-state": "^3.0.0", "sebastian/object-enumerator": "^3.0.3", "sebastian/resource-operations": "^2.0.1", @@ -2659,6 +3032,10 @@ "testing", "xunit" ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.14" + }, "funding": [ { "url": "https://phpunit.de/donate.html", @@ -2669,31 +3046,26 @@ "type": "github" } ], - "time": "2020-06-22T07:06:58+00:00" + "time": "2021-01-17T07:37:30+00:00" }, { "name": "psr/container", - "version": "1.0.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.2.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -2706,7 +3078,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common Container Interface (PHP FIG PSR-11)", @@ -2718,7 +3090,63 @@ "container-interop", "psr" ], - "time": "2017-02-14T16:28:37+00:00" + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, + "time": "2021-03-05T17:36:06+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" }, { "name": "psr/http-message", @@ -2768,6 +3196,9 @@ "request", "response" ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, "time": "2016-08-06T14:39:51+00:00" }, { @@ -2815,6 +3246,9 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.3" + }, "time": "2020-03-23T09:12:05+00:00" }, { @@ -2855,33 +3289,37 @@ } ], "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, "time": "2019-03-08T08:55:37+00:00" }, { "name": "scrutinizer/ocular", - "version": "1.6.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/scrutinizer-ci/ocular.git", - "reference": "3f0bb363ce09a8116d48a25f2a2f5a5fdb5db3aa" + "reference": "801d176fbcee081f1f4f8c879a07a0ca5ff86eca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scrutinizer-ci/ocular/zipball/3f0bb363ce09a8116d48a25f2a2f5a5fdb5db3aa", - "reference": "3f0bb363ce09a8116d48a25f2a2f5a5fdb5db3aa", + "url": "https://api.github.com/repos/scrutinizer-ci/ocular/zipball/801d176fbcee081f1f4f8c879a07a0ca5ff86eca", + "reference": "801d176fbcee081f1f4f8c879a07a0ca5ff86eca", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "~6.3", + "guzzlehttp/guzzle": "~6.3|^7.0", "jms/serializer": "^1.0.0", "phpoption/phpoption": "~1.0", - "symfony/console": "^2.1.0|~3.0|~4.0", - "symfony/process": "~2.3|~3.0|~4.0" + "symfony/console": "^2.1.0|~3.0|~4.0|~5.0", + "symfony/process": "~2.3|~3.0|~4.0|~5.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.2", - "phpunit/phpunit": "^4.8.0", - "symfony/filesystem": "~2.0|~3.0|~4.0" + "phpunit/phpunit": "^9.0.0", + "symfony/filesystem": "~2.0|~3.0|~4.0|~5.0" }, "bin": [ "bin/ocular" @@ -2896,27 +3334,31 @@ "license": [ "Apache-2.0" ], - "time": "2019-06-15T16:36:51+00:00" + "support": { + "issues": "https://github.com/scrutinizer-ci/ocular/issues", + "source": "https://github.com/scrutinizer-ci/ocular/tree/1.8.1" + }, + "time": "2021-02-25T15:54:10+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": ">=5.6" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { @@ -2941,29 +3383,39 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T06:30:41+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:15:22+00:00" }, { "name": "sebastian/comparator", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", - "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758", + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758", "shasum": "" }, "require": { - "php": "^7.1", + "php": ">=7.1", "sebastian/diff": "^3.0", "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^7.1" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { @@ -2981,6 +3433,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -2992,10 +3448,6 @@ { "name": "Bernhard Schussek", "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" } ], "description": "Provides the functionality to compare PHP values for equality", @@ -3005,24 +3457,34 @@ "compare", "equality" ], - "time": "2018-07-12T15:12:46+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:04:30+00:00" }, { "name": "sebastian/diff", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.1" }, "require-dev": { "phpunit/phpunit": "^7.5 || ^8.0", @@ -3044,13 +3506,13 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], "description": "Diff implementation", @@ -3061,24 +3523,34 @@ "unidiff", "unified diff" ], - "time": "2019-02-04T06:01:07+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:59:04+00:00" }, { "name": "sebastian/environment", - "version": "4.2.3", + "version": "4.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", - "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.1" }, "require-dev": { "phpunit/phpunit": "^7.5" @@ -3114,24 +3586,34 @@ "environment", "hhvm" ], - "time": "2019-11-20T08:46:58+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:53:42+00:00" }, { "name": "sebastian/exporter", - "version": "3.1.2", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" + "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", - "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e", + "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e", "shasum": "" }, "require": { - "php": "^7.0", + "php": ">=7.0", "sebastian/recursion-context": "^3.0" }, "require-dev": { @@ -3181,24 +3663,34 @@ "export", "exporter" ], - "time": "2019-09-14T09:02:43+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:47:53+00:00" }, { "name": "sebastian/global-state", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" + "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", - "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b", + "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b", "shasum": "" }, "require": { - "php": "^7.2", + "php": ">=7.2", "sebastian/object-reflector": "^1.1.1", "sebastian/recursion-context": "^3.0" }, @@ -3235,24 +3727,34 @@ "keywords": [ "global state" ], - "time": "2019-02-01T05:30:01+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:43:24+00:00" }, { "name": "sebastian/object-enumerator", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", "shasum": "" }, "require": { - "php": "^7.0", + "php": ">=7.0", "sebastian/object-reflector": "^1.1.1", "sebastian/recursion-context": "^3.0" }, @@ -3282,24 +3784,34 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-08-03T12:35:26+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:40:27+00:00" }, { "name": "sebastian/object-reflector", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "773f97c67f28de00d397be301821b06708fca0be" + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", - "reference": "773f97c67f28de00d397be301821b06708fca0be", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", "shasum": "" }, "require": { - "php": "^7.0" + "php": ">=7.0" }, "require-dev": { "phpunit/phpunit": "^6.0" @@ -3327,24 +3839,34 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "time": "2017-03-29T09:07:27+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:37:18+00:00" }, { "name": "sebastian/recursion-context", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", "shasum": "" }, "require": { - "php": "^7.0" + "php": ">=7.0" }, "require-dev": { "phpunit/phpunit": "^6.0" @@ -3365,14 +3887,14 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, { "name": "Adam Harvey", "email": "aharvey@php.net" @@ -3380,24 +3902,34 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2017-03-03T06:23:57+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:34:24+00:00" }, { "name": "sebastian/resource-operations", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", - "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.1" }, "type": "library", "extra": { @@ -3422,24 +3954,34 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2018-10-04T04:07:39+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:30:19+00:00" }, { "name": "sebastian/type", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" + "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", - "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4", + "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4", "shasum": "" }, "require": { - "php": "^7.2" + "php": ">=7.2" }, "require-dev": { "phpunit/phpunit": "^8.2" @@ -3468,7 +4010,17 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", - "time": "2019-07-02T08:10:15+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/1.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:25:11+00:00" }, { "name": "sebastian/version", @@ -3511,20 +4063,24 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/master" + }, "time": "2016-10-03T07:35:21+00:00" }, { "name": "sempro/phpunit-pretty-print", - "version": "dev-patch-1", + "version": "1.4.0", "source": { "type": "git", - "url": "https://github.com/markcarver/phpunit-pretty-print.git", - "reference": "d90a03400a038d8bb3b9413cfcce83d1274c09ec" + "url": "https://github.com/s360digital/phpunit-pretty-print.git", + "reference": "fa623aa8a17aece4a2b69e54b07a5e37572d1f1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/markcarver/phpunit-pretty-print/zipball/d90a03400a038d8bb3b9413cfcce83d1274c09ec", - "reference": "d90a03400a038d8bb3b9413cfcce83d1274c09ec", + "url": "https://api.github.com/repos/s360digital/phpunit-pretty-print/zipball/fa623aa8a17aece4a2b69e54b07a5e37572d1f1d", + "reference": "fa623aa8a17aece4a2b69e54b07a5e37572d1f1d", "shasum": "" }, "require": { @@ -3540,27 +4096,29 @@ "Sempro\\PHPUnitPrettyPrinter\\": "src/" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "description": "Prettify PHPUnit output", "support": { - "source": "https://github.com/markcarver/phpunit-pretty-print/tree/patch-1" + "issues": "https://github.com/s360digital/phpunit-pretty-print/issues", + "source": "https://github.com/s360digital/phpunit-pretty-print/tree/1.4.0" }, - "time": "2020-09-13T07:13:25+00:00" + "time": "2021-01-04T13:25:10+00:00" }, { "name": "slevomat/coding-standard", - "version": "6.4.0", + "version": "6.4.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "bf3a16a630d8245c350b459832a71afa55c02fd3" + "reference": "696dcca217d0c9da2c40d02731526c1e25b65346" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/bf3a16a630d8245c350b459832a71afa55c02fd3", - "reference": "bf3a16a630d8245c350b459832a71afa55c02fd3", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/696dcca217d0c9da2c40d02731526c1e25b65346", + "reference": "696dcca217d0c9da2c40d02731526c1e25b65346", "shasum": "" }, "require": { @@ -3572,11 +4130,11 @@ "require-dev": { "phing/phing": "2.16.3", "php-parallel-lint/php-parallel-lint": "1.2.0", - "phpstan/phpstan": "0.12.40", + "phpstan/phpstan": "0.12.48", "phpstan/phpstan-deprecation-rules": "0.12.5", "phpstan/phpstan-phpunit": "0.12.16", "phpstan/phpstan-strict-rules": "0.12.5", - "phpunit/phpunit": "7.5.20|8.5.5|9.3.8" + "phpunit/phpunit": "7.5.20|8.5.5|9.4.0" }, "type": "phpcodesniffer-standard", "extra": { @@ -3594,6 +4152,10 @@ "MIT" ], "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/6.4.1" + }, "funding": [ { "url": "https://github.com/kukulich", @@ -3604,20 +4166,20 @@ "type": "tidelift" } ], - "time": "2020-08-31T07:02:52+00:00" + "time": "2020-10-05T12:39:37+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.6", + "version": "3.5.8", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", - "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", "shasum": "" }, "require": { @@ -3655,65 +4217,140 @@ "phpcs", "standards" ], - "time": "2020-08-10T04:50:15+00:00" + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2020-10-23T02:01:07+00:00" }, { "name": "symfony/console", - "version": "v4.4.13", + "version": "v5.2.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727" + "reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/b39fd99b9297b67fb7633b7d8083957a97e1e727", - "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727", + "url": "https://api.github.com/repos/symfony/console/zipball/938ebbadae1b0a9c9d1ec313f87f9708609f1b79", + "reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", "symfony/polyfill-php80": "^1.15", - "symfony/service-contracts": "^1.1|^2" + "symfony/service-contracts": "^1.1|^2", + "symfony/string": "^5.1" }, "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/event-dispatcher": "<4.3|>=5", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", "symfony/lock": "<4.4", - "symfony/process": "<3.3" + "symfony/process": "<4.4" }, "provide": { "psr/log-implementation": "1.0" }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/event-dispatcher": "^4.3", - "symfony/lock": "^4.4|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", - "symfony/var-dumper": "^4.3|^5.0" + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0", + "symfony/var-dumper": "^4.4|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-06T13:42:15+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "shasum": "" }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" + "require": { + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } }, "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3722,16 +4359,19 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Console Component", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/master" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3746,52 +4386,50 @@ "type": "tidelift" } ], - "time": "2020-09-02T07:07:21+00:00" + "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.13", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030" + "reference": "d08d6ec121a425897951900ab692b612a61d6240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3e8ea5ccddd00556b86d69d42f99f1061a704030", - "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d08d6ec121a425897951900ab692b612a61d6240", + "reference": "d08d6ec121a425897951900ab692b612a61d6240", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/event-dispatcher-contracts": "^1.1" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/event-dispatcher-contracts": "^2", + "symfony/polyfill-php80": "^1.15" }, "conflict": { - "symfony/dependency-injection": "<3.4" + "symfony/dependency-injection": "<4.4" }, "provide": { "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "1.1" + "symfony/event-dispatcher-implementation": "2.0" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/error-handler": "^4.4|^5.0", + "symfony/expression-language": "^4.4|^5.0", + "symfony/http-foundation": "^4.4|^5.0", "symfony/service-contracts": "^1.1|^2", - "symfony/stopwatch": "^3.4|^4.0|^5.0" + "symfony/stopwatch": "^4.4|^5.0" }, "suggest": { "symfony/dependency-injection": "", "symfony/http-kernel": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" @@ -3814,8 +4452,11 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony EventDispatcher Component", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.4" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3830,33 +4471,33 @@ "type": "tidelift" } ], - "time": "2020-08-13T14:18:44+00:00" + "time": "2021-02-18T17:12:37+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v1.1.9", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7" + "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/84e23fdcd2517bf37aecbd16967e83f0caee25a7", - "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0ba7d54483095a198fa51781bc608d17e84dffa2", + "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2", "shasum": "" }, "require": { - "php": ">=7.1.3" + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" }, "suggest": { - "psr/event-dispatcher": "", "symfony/event-dispatcher-implementation": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "2.2-dev" }, "thanks": { "name": "symfony/contracts", @@ -3892,6 +4533,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.2.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3906,20 +4550,20 @@ "type": "tidelift" } ], - "time": "2020-07-06T13:19:58+00:00" + "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/filesystem", - "version": "v5.1.5", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "f7b9ed6142a34252d219801d9767dedbd711da1a" + "reference": "710d364200997a5afde34d9fe57bd52f3cc1e108" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/f7b9ed6142a34252d219801d9767dedbd711da1a", - "reference": "f7b9ed6142a34252d219801d9767dedbd711da1a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/710d364200997a5afde34d9fe57bd52f3cc1e108", + "reference": "710d364200997a5afde34d9fe57bd52f3cc1e108", "shasum": "" }, "require": { @@ -3927,11 +4571,6 @@ "symfony/polyfill-ctype": "~1.8" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Filesystem\\": "" @@ -3954,8 +4593,11 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Filesystem Component", + "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.2.4" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3970,34 +4612,93 @@ "type": "tidelift" } ], - "time": "2020-08-21T17:19:47+00:00" + "time": "2021-02-12T10:38:38+00:00" }, { "name": "symfony/finder", - "version": "v5.1.5", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2b765f0cf6612b3636e738c0689b29aa63088d5d" + "reference": "0d639a0943822626290d169965804f79400e6a04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2b765f0cf6612b3636e738c0689b29aa63088d5d", - "reference": "2b765f0cf6612b3636e738c0689b29aa63088d5d", + "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", + "reference": "0d639a0943822626290d169965804f79400e6a04", "shasum": "" }, "require": { "php": ">=7.2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } + ], + "time": "2021-02-15T18:55:04+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce", + "reference": "5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php73": "~1.0", + "symfony/polyfill-php80": "^1.15" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Finder\\": "" + "Symfony\\Component\\OptionsResolver\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -4017,8 +4718,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Finder Component", + "description": "Provides an improved replacement for the array_replace PHP function", "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v5.2.4" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4033,24 +4742,24 @@ "type": "tidelift" } ], - "time": "2020-08-17T10:01:29+00:00" + "time": "2021-01-27T12:56:27+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.18.1", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-ctype": "For best performance" @@ -4058,7 +4767,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4095,6 +4804,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4109,27 +4821,24 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.18.1", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.22.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/5dcab1bc7146cf8c1beaa4502a3d9be344334251", - "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170", + "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170", "shasum": "" }, "require": { - "php": ">=5.3.3", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php70": "^1.10", - "symfony/polyfill-php72": "^1.10" + "php": ">=7.1" }, "suggest": { "ext-intl": "For best performance" @@ -4137,7 +4846,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4146,7 +4855,7 @@ }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" }, "files": [ "bootstrap.php" @@ -4158,28 +4867,27 @@ ], "authors": [ { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "description": "Symfony polyfill for intl's grapheme_* functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "idn", + "grapheme", "intl", "polyfill", "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.1" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4194,24 +4902,24 @@ "type": "tidelift" } ], - "time": "2020-08-04T06:02:08+00:00" + "time": "2021-01-22T09:19:47+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.18.1", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-intl": "For best performance" @@ -4219,7 +4927,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4261,6 +4969,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4275,24 +4986,24 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2021-01-22T09:19:47+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.18.1", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-mbstring": "For best performance" @@ -4300,7 +5011,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4338,6 +5049,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4352,47 +5066,35 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2021-01-22T09:19:47+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3" + "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", - "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/5f03a781d984aae42cebd18e7912fa80f02ee644", + "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644", "shasum": "" }, "require": { - "paragonie/random_compat": "~1.0|~2.0|~9.99", - "php": ">=5.3.3" + "php": ">=7.1" }, - "type": "library", + "type": "metapackage", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php70\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -4415,6 +5117,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php70/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4429,29 +5134,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.18.1", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "639447d008615574653fb3bc60d1986d7172eaae" + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", - "reference": "639447d008615574653fb3bc60d1986d7172eaae", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4488,6 +5193,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.22.1" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4502,29 +5210,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.18.1", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca" + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca", - "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4564,6 +5272,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4578,31 +5289,27 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/process", - "version": "v4.4.13", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479" + "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/65e70bab62f3da7089a8d4591fb23fbacacb3479", - "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479", + "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", + "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", "shasum": "" }, "require": { - "php": ">=7.1.3" + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.15" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" @@ -4625,8 +5332,11 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Process Component", + "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.2.4" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4641,7 +5351,7 @@ "type": "tidelift" } ], - "time": "2020-07-23T08:31:43+00:00" + "time": "2021-01-27T10:15:41+00:00" }, { "name": "symfony/service-contracts", @@ -4703,6 +5413,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/master" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4721,16 +5434,16 @@ }, { "name": "symfony/stopwatch", - "version": "v5.1.5", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323" + "reference": "b12274acfab9d9850c52583d136a24398cdf1a0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/0f7c58cf81dbb5dd67d423a89d577524a2ec0323", - "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b12274acfab9d9850c52583d136a24398cdf1a0c", + "reference": "b12274acfab9d9850c52583d136a24398cdf1a0c", "shasum": "" }, "require": { @@ -4738,11 +5451,6 @@ "symfony/service-contracts": "^1.0|^2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Stopwatch\\": "" @@ -4765,8 +5473,94 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Stopwatch Component", + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v5.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-27T10:15:41+00:00" + }, + { + "name": "symfony/string", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "4e78d7d47061fa183639927ec40d607973699609" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/4e78d7d47061fa183639927ec40d607973699609", + "reference": "4e78d7d47061fa183639927ec40d607973699609", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.2.4" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4781,7 +5575,7 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2021-02-16T10:20:28+00:00" }, { "name": "theseer/tokenizer", @@ -4821,6 +5615,10 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/master" + }, "funding": [ { "url": "https://github.com/theseer", @@ -4831,23 +5629,23 @@ }, { "name": "unleashedtech/php-coding-standard", - "version": "v2.5.0", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/unleashedtech/php-coding-standard.git", - "reference": "521fe5cc70c1281e37f0c4c307d968d7492d0d9a" + "reference": "e33899854ba672225898d1a1020cf139fbe254ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/unleashedtech/php-coding-standard/zipball/521fe5cc70c1281e37f0c4c307d968d7492d0d9a", - "reference": "521fe5cc70c1281e37f0c4c307d968d7492d0d9a", + "url": "https://api.github.com/repos/unleashedtech/php-coding-standard/zipball/e33899854ba672225898d1a1020cf139fbe254ea", + "reference": "e33899854ba672225898d1a1020cf139fbe254ea", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "php": "^7.1", - "slevomat/coding-standard": "~6.4.0", - "squizlabs/php_codesniffer": "^3.4.0" + "php": "^7.1 || ^8.0", + "slevomat/coding-standard": "~6.4.1", + "squizlabs/php_codesniffer": "^3.5.8" }, "require-dev": { "symfony/phpunit-bridge": "^5.1" @@ -4855,7 +5653,7 @@ "type": "phpcodesniffer-standard", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -4879,20 +5677,24 @@ "Coding Standard", "phpcs" ], - "time": "2020-09-08T20:40:50+00:00" + "support": { + "issues": "https://github.com/unleashedtech/php-coding-standard/issues", + "source": "https://github.com/unleashedtech/php-coding-standard" + }, + "time": "2021-03-05T18:03:48+00:00" }, { "name": "vimeo/psalm", - "version": "3.16", + "version": "3.18.2", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "d03e5ef057d6adc656c0ff7e166c50b73b4f48f3" + "reference": "19aa905f7c3c7350569999a93c40ae91ae4e1626" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d03e5ef057d6adc656c0ff7e166c50b73b4f48f3", - "reference": "d03e5ef057d6adc656c0ff7e166c50b73b4f48f3", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/19aa905f7c3c7350569999a93c40ae91ae4e1626", + "reference": "19aa905f7c3c7350569999a93c40ae91ae4e1626", "shasum": "" }, "require": { @@ -4979,19 +5781,23 @@ "inspection", "php" ], - "time": "2020-09-15T13:39:50+00:00" + "support": { + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm/tree/3.18.2" + }, + "time": "2020-10-20T13:48:22+00:00" }, { "name": "webmozart/assert", "version": "1.9.1", "source": { "type": "git", - "url": "https://github.com/webmozart/assert.git", + "url": "https://github.com/webmozarts/assert.git", "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", "shasum": "" }, @@ -5028,6 +5834,10 @@ "check", "validate" ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.9.1" + }, "time": "2020-07-08T17:02:28+00:00" }, { @@ -5035,12 +5845,12 @@ "version": "4.1.0", "source": { "type": "git", - "url": "https://github.com/webmozart/glob.git", + "url": "https://github.com/webmozarts/glob.git", "reference": "3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/glob/zipball/3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe", + "url": "https://api.github.com/repos/webmozarts/glob/zipball/3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe", "reference": "3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe", "shasum": "" }, @@ -5075,6 +5885,10 @@ } ], "description": "A PHP implementation of Ant's glob.", + "support": { + "issues": "https://github.com/webmozarts/glob/issues", + "source": "https://github.com/webmozarts/glob/tree/4.1.0" + }, "time": "2015-12-29T11:14:33+00:00" }, { @@ -5121,21 +5935,26 @@ } ], "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.", + "support": { + "issues": "https://github.com/webmozart/path-util/issues", + "source": "https://github.com/webmozart/path-util/tree/2.3.0" + }, "time": "2015-12-17T08:42:14+00:00" } ], "aliases": [], "minimum-stability": "dev", "stability-flags": { - "sempro/phpunit-pretty-print": 20 + "league/configuration": 20 }, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^7.2.5 || ^8.0", + "ext-json": "*", "ext-mbstring": "*", "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/datasets/da/cldr.gz b/datasets/da/cldr.gz index 9c4b483..5ff295f 100644 Binary files a/datasets/da/cldr.gz and b/datasets/da/cldr.gz differ diff --git a/datasets/de/cldr.gz b/datasets/de/cldr.gz index 91f90ba..2c3d1d3 100644 Binary files a/datasets/de/cldr.gz and b/datasets/de/cldr.gz differ diff --git a/datasets/en-gb/cldr.gz b/datasets/en-gb/cldr.gz index d2c50de..2dd7a96 100644 Binary files a/datasets/en-gb/cldr.gz and b/datasets/en-gb/cldr.gz differ diff --git a/datasets/en-gb/emojibase.gz b/datasets/en-gb/emojibase.gz new file mode 100644 index 0000000..03ef973 Binary files /dev/null and b/datasets/en-gb/emojibase.gz differ diff --git a/datasets/en/cldr.gz b/datasets/en/cldr.gz index b56429b..e29895e 100644 Binary files a/datasets/en/cldr.gz and b/datasets/en/cldr.gz differ diff --git a/datasets/en/emojibase-legacy.gz b/datasets/en/emojibase-legacy.gz index 89dcfe9..e5cbaee 100644 Binary files a/datasets/en/emojibase-legacy.gz and b/datasets/en/emojibase-legacy.gz differ diff --git a/datasets/en/emojibase.gz b/datasets/en/emojibase.gz index edda418..418f4b2 100644 Binary files a/datasets/en/emojibase.gz and b/datasets/en/emojibase.gz differ diff --git a/datasets/en/github.gz b/datasets/en/github.gz index e34fdb7..a4d04d7 100644 Binary files a/datasets/en/github.gz and b/datasets/en/github.gz differ diff --git a/datasets/en/iamcal.gz b/datasets/en/iamcal.gz index cd4e209..079d389 100644 Binary files a/datasets/en/iamcal.gz and b/datasets/en/iamcal.gz differ diff --git a/datasets/en/joypixels.gz b/datasets/en/joypixels.gz index f269bc0..7a31e5e 100644 Binary files a/datasets/en/joypixels.gz and b/datasets/en/joypixels.gz differ diff --git a/datasets/es-mx/cldr.gz b/datasets/es-mx/cldr.gz index 5734dd2..755c266 100644 Binary files a/datasets/es-mx/cldr.gz and b/datasets/es-mx/cldr.gz differ diff --git a/datasets/es/cldr.gz b/datasets/es/cldr.gz index 595d151..744fc32 100644 Binary files a/datasets/es/cldr.gz and b/datasets/es/cldr.gz differ diff --git a/datasets/et/cldr.gz b/datasets/et/cldr.gz index 4f9812e..2a11b21 100644 Binary files a/datasets/et/cldr.gz and b/datasets/et/cldr.gz differ diff --git a/datasets/fi/cldr.gz b/datasets/fi/cldr.gz index 55b2f57..437668e 100644 Binary files a/datasets/fi/cldr.gz and b/datasets/fi/cldr.gz differ diff --git a/datasets/fr/cldr.gz b/datasets/fr/cldr.gz index 6b4d6a6..2f65e6e 100644 Binary files a/datasets/fr/cldr.gz and b/datasets/fr/cldr.gz differ diff --git a/datasets/fr/emojibase.gz b/datasets/fr/emojibase.gz new file mode 100644 index 0000000..7a4f147 Binary files /dev/null and b/datasets/fr/emojibase.gz differ diff --git a/datasets/hu/cldr.gz b/datasets/hu/cldr.gz index 7febc9e..eed0c53 100644 Binary files a/datasets/hu/cldr.gz and b/datasets/hu/cldr.gz differ diff --git a/datasets/it/cldr.gz b/datasets/it/cldr.gz index abae36c..f16677d 100644 Binary files a/datasets/it/cldr.gz and b/datasets/it/cldr.gz differ diff --git a/datasets/ja/cldr-native.gz b/datasets/ja/cldr-native.gz index a768af9..7642b0b 100644 Binary files a/datasets/ja/cldr-native.gz and b/datasets/ja/cldr-native.gz differ diff --git a/datasets/ja/cldr.gz b/datasets/ja/cldr.gz index f2133d4..b5f4534 100644 Binary files a/datasets/ja/cldr.gz and b/datasets/ja/cldr.gz differ diff --git a/datasets/ja/emojibase.gz b/datasets/ja/emojibase.gz new file mode 100644 index 0000000..ccd21c3 Binary files /dev/null and b/datasets/ja/emojibase.gz differ diff --git a/datasets/ko/cldr-native.gz b/datasets/ko/cldr-native.gz index 628862c..4e1592f 100644 Binary files a/datasets/ko/cldr-native.gz and b/datasets/ko/cldr-native.gz differ diff --git a/datasets/ko/cldr.gz b/datasets/ko/cldr.gz index cb7ed63..bdd84fe 100644 Binary files a/datasets/ko/cldr.gz and b/datasets/ko/cldr.gz differ diff --git a/datasets/lt/cldr.gz b/datasets/lt/cldr.gz index 9792e11..03ff12b 100644 Binary files a/datasets/lt/cldr.gz and b/datasets/lt/cldr.gz differ diff --git a/datasets/ms/cldr.gz b/datasets/ms/cldr.gz index 36e4da8..0c22c3c 100644 Binary files a/datasets/ms/cldr.gz and b/datasets/ms/cldr.gz differ diff --git a/datasets/nb/cldr.gz b/datasets/nb/cldr.gz index 00c91ef..ca097ed 100644 Binary files a/datasets/nb/cldr.gz and b/datasets/nb/cldr.gz differ diff --git a/datasets/nl/cldr.gz b/datasets/nl/cldr.gz index 2735257..967033c 100644 Binary files a/datasets/nl/cldr.gz and b/datasets/nl/cldr.gz differ diff --git a/datasets/pl/cldr.gz b/datasets/pl/cldr.gz index 3b2f569..ca0a8fa 100644 Binary files a/datasets/pl/cldr.gz and b/datasets/pl/cldr.gz differ diff --git a/datasets/pt/cldr.gz b/datasets/pt/cldr.gz index 98e9b4d..fcfbbf1 100644 Binary files a/datasets/pt/cldr.gz and b/datasets/pt/cldr.gz differ diff --git a/datasets/ru/cldr-native.gz b/datasets/ru/cldr-native.gz index e1024e9..949bcdd 100644 Binary files a/datasets/ru/cldr-native.gz and b/datasets/ru/cldr-native.gz differ diff --git a/datasets/ru/cldr.gz b/datasets/ru/cldr.gz index b09c949..2070ecb 100644 Binary files a/datasets/ru/cldr.gz and b/datasets/ru/cldr.gz differ diff --git a/datasets/ru/emojibase.gz b/datasets/ru/emojibase.gz new file mode 100644 index 0000000..8889c37 Binary files /dev/null and b/datasets/ru/emojibase.gz differ diff --git a/datasets/sv/cldr.gz b/datasets/sv/cldr.gz index d2373dd..53979c1 100644 Binary files a/datasets/sv/cldr.gz and b/datasets/sv/cldr.gz differ diff --git a/datasets/th/cldr-native.gz b/datasets/th/cldr-native.gz index 7806b0a..68d992a 100644 Binary files a/datasets/th/cldr-native.gz and b/datasets/th/cldr-native.gz differ diff --git a/datasets/th/cldr.gz b/datasets/th/cldr.gz index 539ae02..16cefa7 100644 Binary files a/datasets/th/cldr.gz and b/datasets/th/cldr.gz differ diff --git a/datasets/uk/cldr-native.gz b/datasets/uk/cldr-native.gz index eb002e5..53c9e83 100644 Binary files a/datasets/uk/cldr-native.gz and b/datasets/uk/cldr-native.gz differ diff --git a/datasets/uk/cldr.gz b/datasets/uk/cldr.gz index db2d647..f3d49f3 100644 Binary files a/datasets/uk/cldr.gz and b/datasets/uk/cldr.gz differ diff --git a/datasets/zh-hant/cldr-native.gz b/datasets/zh-hant/cldr-native.gz index 6c6cea0..8e3da74 100644 Binary files a/datasets/zh-hant/cldr-native.gz and b/datasets/zh-hant/cldr-native.gz differ diff --git a/datasets/zh-hant/cldr.gz b/datasets/zh-hant/cldr.gz index 385085b..41fbc54 100644 Binary files a/datasets/zh-hant/cldr.gz and b/datasets/zh-hant/cldr.gz differ diff --git a/datasets/zh/cldr-native.gz b/datasets/zh/cldr-native.gz index b7b4a22..3b4da24 100644 Binary files a/datasets/zh/cldr-native.gz and b/datasets/zh/cldr-native.gz differ diff --git a/datasets/zh/cldr.gz b/datasets/zh/cldr.gz index a9f300c..a8b68c1 100644 Binary files a/datasets/zh/cldr.gz and b/datasets/zh/cldr.gz differ diff --git a/datasets/zh/emojibase.gz b/datasets/zh/emojibase.gz new file mode 100644 index 0000000..f51eafb Binary files /dev/null and b/datasets/zh/emojibase.gz differ diff --git a/package-lock.json b/package-lock.json index ff9f480..98e5859 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,71 @@ { "name": "@unicorn-fail/emoji", "version": "0.0.1", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "@unicorn-fail/emoji", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "emojibase": "^5.0.0-alpha.1", + "emojibase-data": "^6.0.0-alpha.1", + "emojibase-regex": "^5.0.0-alpha.0" + } + }, + "node_modules/emojibase": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/emojibase/-/emojibase-5.1.1.tgz", + "integrity": "sha512-qc/qfuzZ93xmYMhnVA15yP2k3t4zJvBbqCc8dpDXFf75eJ+IogbzJuWQur8fkwsZ0u7YvJDBh939F9DapQqktQ==", + "dev": true, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/milesjohnson" + } + }, + "node_modules/emojibase-data": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/emojibase-data/-/emojibase-data-6.1.2.tgz", + "integrity": "sha512-1fYgpAYkAJq7bmGO3XboP0dQHrtRiyWrAluCmv5NHSK1nruz0x8kXhpLiz48rAJGDQOWYUpLGaPASTNXVZX87Q==", + "dev": true, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/milesjohnson" + }, + "peerDependencies": { + "emojibase": "*" + } + }, + "node_modules/emojibase-regex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/emojibase-regex/-/emojibase-regex-5.1.2.tgz", + "integrity": "sha512-/n1k9npdzvJecnTy7qu324btGxWSi4GC4Uk9R4DLDc8CIuNx+a2RQLrDSL/0kOi0dIg4lP4h65wKICYGjyIDbw==", + "dev": true, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/milesjohnson" + } + } + }, "dependencies": { "emojibase": { - "version": "5.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/emojibase/-/emojibase-5.0.0-alpha.1.tgz", - "integrity": "sha512-hi46OE3FQCjttApjb8F10g5aaUCSm0BOy3s5KFlXTkhCC6VgfRsvVTvcE88jvBkftEeGVuPdBOVYkAIX89tJhw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/emojibase/-/emojibase-5.1.1.tgz", + "integrity": "sha512-qc/qfuzZ93xmYMhnVA15yP2k3t4zJvBbqCc8dpDXFf75eJ+IogbzJuWQur8fkwsZ0u7YvJDBh939F9DapQqktQ==", "dev": true }, "emojibase-data": { - "version": "6.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/emojibase-data/-/emojibase-data-6.0.0-alpha.1.tgz", - "integrity": "sha512-gSZkFdgyjXx2J4ASRPjq9iHDEktSyh3bjQEIJYbOBtAK/g7Ax46v6Vf8JtXwOmccA8hdoCOc5Nb83iZ4wPAQuw==", - "dev": true + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/emojibase-data/-/emojibase-data-6.1.2.tgz", + "integrity": "sha512-1fYgpAYkAJq7bmGO3XboP0dQHrtRiyWrAluCmv5NHSK1nruz0x8kXhpLiz48rAJGDQOWYUpLGaPASTNXVZX87Q==", + "dev": true, + "requires": {} }, "emojibase-regex": { - "version": "5.0.0-alpha.0", - "resolved": "https://registry.npmjs.org/emojibase-regex/-/emojibase-regex-5.0.0-alpha.0.tgz", - "integrity": "sha512-8zrgL10Q8PWemXVOpKsquvg6/pbv3ce+Z/5fwilwbd4sF5XJyUXDpn3mKm48TlPUST6eZldChPtO/Y07QrAITw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/emojibase-regex/-/emojibase-regex-5.1.2.tgz", + "integrity": "sha512-/n1k9npdzvJecnTy7qu324btGxWSi4GC4Uk9R4DLDc8CIuNx+a2RQLrDSL/0kOi0dIg4lP4h65wKICYGjyIDbw==", "dev": true } } diff --git a/scripts/build.js b/scripts/build.js index 82b67ac..c195122 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -7,27 +7,27 @@ const emojibaseShortcode = require('./lib/emojibase-shortcode') const emojibaseSkins = require('./lib/emojibase-skins') const PhpInterface = require('./lib/php-interface') -PhpInterface.create('UnicornFail\\Emoji\\Emojibase\\DatasetInterface') +PhpInterface.create('UnicornFail\\Emoji\\Emojibase\\EmojibaseDatasetInterface') .setModule('emojibase') .addConstants(emojibaseDataset) .write() -PhpInterface.create('UnicornFail\\Emoji\\Emojibase\\ShortcodeInterface') +PhpInterface.create('UnicornFail\\Emoji\\Emojibase\\EmojibaseShortcodeInterface') .setModule('emojibase') .addConstants(emojibaseShortcode) .write() -PhpInterface.create('UnicornFail\\Emoji\\Emojibase\\SkinsInterface') +PhpInterface.create('UnicornFail\\Emoji\\Emojibase\\EmojibaseSkinsInterface') .setModule('emojibase') .addConstants(emojibaseSkins) .write() -PhpInterface.create('UnicornFail\\Emoji\\Emojibase\\GroupsInterface') +PhpInterface.create('UnicornFail\\Emoji\\Emojibase\\EmojibaseGroupsInterface') .setModule('emojibase') .addConstants(emojibaseGroups) .write() -PhpInterface.create('UnicornFail\\Emoji\\Emojibase\\RegexInterface') +PhpInterface.create('UnicornFail\\Emoji\\Emojibase\\EmojibaseRegexInterface') .setModule('emojibase-regex') .addConstants(emojibaseRegex) .write() diff --git a/scripts/build.php b/scripts/build.php index e6b5e77..3ace24e 100755 --- a/scripts/build.php +++ b/scripts/build.php @@ -6,22 +6,23 @@ const BASE_DIRECTORY = __DIR__ . '/../'; require_once BASE_DIRECTORY . '/vendor/autoload.php'; -use UnicornFail\Emoji\Dataset; -use UnicornFail\Emoji\Emojibase\DatasetInterface; -use UnicornFail\Emoji\Emojibase\ShortcodeInterface; +use UnicornFail\Emoji\Dataset\Dataset; +use UnicornFail\Emoji\Dataset\RuntimeDataset; +use UnicornFail\Emoji\Emojibase\EmojibaseDatasetInterface; +use UnicornFail\Emoji\Emojibase\EmojibaseShortcodeInterface; use UnicornFail\Emoji\Util\Normalize; const BUILD_DIRECTORY = BASE_DIRECTORY . '/build'; const EMOJIBASE_DATA_DIRECTORY = BASE_DIRECTORY . '/node_modules/emojibase-data'; -if (! is_dir(EMOJIBASE_DATA_DIRECTORY) || ! interface_exists(DatasetInterface::class)) { +if (! is_dir(EMOJIBASE_DATA_DIRECTORY) || ! interface_exists(EmojibaseDatasetInterface::class)) { throw new \RuntimeException('You must first run `npm install && npm run build` to build the datasets.'); } /** * @param mixed[] $shortcodes */ -function create_dataset(string $locale, array $shortcodes): Dataset +function create_dataset(string $locale, string $preset, array $shortcodes): Dataset { $data = null; @@ -86,8 +87,8 @@ function create_dataset(string $locale, array $shortcodes): Dataset $baseDirectory = realpath(BASE_DIRECTORY); // Archive datasets. -foreach (DatasetInterface::SUPPORTED_LOCALES as $locale) { - foreach (ShortcodeInterface::PRESETS as $preset) { +foreach (EmojibaseDatasetInterface::SUPPORTED_LOCALES as $locale) { + foreach (EmojibaseShortcodeInterface::PRESETS as $preset) { // Skip presets that don't exist. $file = sprintf('%s/%s/shortcodes/%s.json', EMOJIBASE_DATA_DIRECTORY, $locale, $preset); if (! file_exists($file) || ! ($c = file_get_contents($file)) || ! ($shortcodes = json_decode($c, true))) { @@ -95,13 +96,13 @@ function create_dataset(string $locale, array $shortcodes): Dataset } $destination = sprintf('%s/%s/%s.gz', Dataset::DIRECTORY, $locale, $preset); - $relative = str_replace($baseDirectory . '/src/..', '.', $destination); + $relative = str_replace($baseDirectory . '/src/Dataset/../..', '.', $destination); $directory = dirname($destination); /** @scrutinizer ignore-unhandled */ @mkdir($directory, 0775, true); echo sprintf("Archiving %s\n", $relative); - $dataset = create_dataset($locale, $shortcodes); - file_put_contents($destination, $dataset->archive()); + $dataset = create_dataset($locale, $preset, $shortcodes); + file_put_contents($destination, RuntimeDataset::archive($dataset)); } } diff --git a/scripts/lib/php-file.js b/scripts/lib/php-file.js index 2b1bd36..2f11cf0 100644 --- a/scripts/lib/php-file.js +++ b/scripts/lib/php-file.js @@ -42,11 +42,16 @@ const createValue = (indentLevel, value, currentLineLength = 0) => { return value ? 'true' : 'false' } if (value instanceof RegExp) { + // Add the unicode modifier if unicode codepoints are found, but it wasn't set. + if (/\\u[0-9A-F]{4,5}/.test(value.source) && !value.unicode) { + value = new RegExp(value.source, value.flags + 'u') + } + // PHP uses a different unicode codepoint syntax, adjust to match. value = value.toString() // Unicode codepoint. .replace(/\\u{([0-9A-F]{5})}/g, '\\x{$1}') - // Unicode hex + // Unicode hex. .replace(/\\u([0-9A-F]{4})/g, '\\x{$1}') } diff --git a/src/Configuration.php b/src/Configuration.php deleted file mode 100644 index 3791ca2..0000000 --- a/src/Configuration.php +++ /dev/null @@ -1,190 +0,0 @@ -getArrayCopy() : []; - foreach ($data as $key => $value) { - $this->set((string) $key, $value); - } - - try { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $this->data = $resolver->resolve($this->data); - } catch (\Throwable $throwable) { - throw new InvalidConfigurationException($throwable->getMessage(), (int) $throwable->getCode(), $throwable->getPrevious()); - } - } - - /** - * @param mixed[]|\Traversable $configuration - */ - public static function create(?iterable $configuration = null): ConfigurationInterface - { - if ($configuration instanceof ConfigurationInterface) { - return $configuration; - } - - return new self($configuration); - } - - public function configureOptions(OptionsResolver $resolver): void - { - $this->defineConvertEmoticons($resolver); - $this->defineExclude($resolver); - $this->defineLocale($resolver); - $this->defineNative($resolver); - $this->definePresentation($resolver); - $this->definePreset($resolver); - $this->defineStringableType($resolver); - } - - protected function defineConvertEmoticons(OptionsResolver $resolver): void - { - $resolver->define('convertEmoticons') - ->allowedTypes('bool') - ->default(true); - } - - protected function defineExclude(OptionsResolver $resolver): void - { - $resolver->setDefault('exclude', static function (OptionsResolver $resolver): void { - $resolver->define('shortcodes') - ->allowedTypes('string', 'string[]') - ->default([]) - ->normalize( - /** - * @param string|string[] $value - * - * @return string[] - */ - static function (Options $options, $value): array { - return Normalize::shortcodes($value); - } - ); - }); - } - - protected function defineLocale(OptionsResolver $resolver): void - { - $resolver->define('locale') - ->allowedTypes('string') - ->allowedValues(static function (string $value): bool { - return ! ! Normalize::locale($value); - }) - ->default('en') - ->normalize(static function (Options $options, string $value): string { - return Normalize::locale($value); - }); - } - - protected function defineNative(OptionsResolver $resolver): void - { - $resolver->define('native') - ->allowedTypes('bool') - ->default(static function (Options $options): bool { - return \in_array($options['locale'], DatasetInterface::NON_LATIN_LOCALES, true); - }) - ->normalize(static function (Options $options, bool $value) { - return $value && \in_array($options['locale'], DatasetInterface::NON_LATIN_LOCALES, true); - }); - } - - protected function definePresentation(OptionsResolver $resolver): void - { - $resolver->define('presentation') - ->allowedTypes('int', 'null') - ->allowedValues(...DatasetInterface::SUPPORTED_PRESENTATIONS) - ->default(DatasetInterface::EMOJI); - } - - protected function definePreset(OptionsResolver $resolver): void - { - $resolver->define('preset') - ->allowedTypes('string', 'string[]') - ->allowedValues(\Closure::fromCallable([$this, 'definePresetAllowedValues'])) - ->default(ShortcodeInterface::DEFAULT_PRESETS) - ->normalize(\Closure::fromCallable([$this, 'definePresetNormalize'])); - } - - /** - * @param mixed $values - */ - protected function definePresetAllowedValues($values): bool - { - foreach ((array) $values as $value) { - \assert(\is_string($value)); - if (! \in_array($value, ShortcodeInterface::SUPPORTED_PRESETS, true)) { - throw new InvalidOptionsException(\sprintf( - 'The option "preset" with value "%s" is invalid. Accepted values are: %s.', - $value, - \implode(', ', \array_map(static function ($s) { - return \sprintf('"%s"', $s); - }, ShortcodeInterface::SUPPORTED_PRESETS)) - )); - } - } - - return true; - } - - /** - * @param mixed $value - * - * @return string[] - */ - protected function definePresetNormalize(Options $options, $value): array - { - // Presets. - $presets = []; - foreach ((array) $value as $preset) { - \assert(\is_string($preset)); - if (isset(ShortcodeInterface::PRESET_ALIASES[$preset])) { - $presets[] = ShortcodeInterface::PRESET_ALIASES[$preset]; - } elseif (isset(ShortcodeInterface::PRESETS[$preset])) { - $presets[] = ShortcodeInterface::PRESETS[$preset]; - } - } - - // Prepend the native preset if local is requires it and enabled. - if ($options['native']) { - \array_unshift($presets, ShortcodeInterface::PRESET_CLDR_NATIVE); - } - - return \array_values(\array_unique($presets)); - } - - protected function defineStringableType(OptionsResolver $resolver): void - { - $resolver->define('stringableType') - ->allowedTypes('int') - ->allowedValues(Lexer::T_EMOTICON, Lexer::T_HTML_ENTITY, Lexer::T_SHORTCODE, Lexer::T_UNICODE) - ->default(Lexer::T_UNICODE); - } - - public function getIterator(): \ArrayObject - { - return new \ArrayObject($this->export()); - } -} diff --git a/src/ConfigurationInterface.php b/src/ConfigurationInterface.php deleted file mode 100644 index a49ef53..0000000 --- a/src/ConfigurationInterface.php +++ /dev/null @@ -1,13 +0,0 @@ -parser = $parser ?? new Parser($configuration, $dataset); - } - - /** - * @param mixed[]|\Traversable $configuration - */ - public static function create(?iterable $configuration = null): self - { - return new self($configuration); - } - - public function convert(string $input, ?int $type = null): string - { - $parser = $this->getParser(); - $stringableType = (int) $parser->getConfiguration()->get('stringableType'); - - // Parse. - $tokens = $parser->parse($input); - - // Ensure tokens are set to the correct stringable type. - if ($type !== null && $type !== $stringableType) { - foreach (AbstractEmojiToken::filter($tokens) as $token) { - $token->setStringableType($type); - } - } - - return \implode($tokens); - } - - public function convertToEmoticon(string $input): string - { - return $this->convert($input, Lexer::T_EMOTICON); - } - - public function convertToHtml(string $input): string - { - return $this->convert($input, Lexer::T_HTML_ENTITY); - } - - public function convertToShortcode(string $input): string - { - return $this->convert($input, Lexer::T_SHORTCODE); - } - - public function convertToUnicode(string $input): string - { - return $this->convert($input, Lexer::T_UNICODE); - } - - public function getParser(): ParserInterface - { - return $this->parser; - } -} diff --git a/src/Dataset.php b/src/Dataset.php deleted file mode 100644 index a998018..0000000 --- a/src/Dataset.php +++ /dev/null @@ -1,110 +0,0 @@ -index = $index; - parent::__construct(Normalize::dataset($emojis, $index), \ArrayIterator::ARRAY_AS_PROPS | \ArrayIterator::STD_PROP_LIST); - } - - public static function unarchive(string $filename): self - { - if (! \file_exists($filename)) { - throw new FileNotFoundException($filename); - } - - if ( - ! ($contents = \file_get_contents($filename)) || - ! ($decoded = \gzdecode($contents)) - ) { - throw new UnarchiveException($filename); - } - - try { - /** @var ?Dataset $dataset */ - $dataset = \unserialize((string) $decoded); - } catch (\Throwable $throwable) { - throw new MalformedArchiveException($filename, $throwable); - } - - if (! $dataset instanceof Dataset) { - throw new MalformedArchiveException($filename); - } - - return $dataset; - } - - /** - * @param string[] $indices - * - * @return false|string - */ - public function archive(array $indices = Parser::INDICES) - { - foreach ($indices as $index) { - $this->indexBy($index); - } - - $serialize = \serialize($this); - - return \gzencode($serialize, 9); - } - - /** - * @param callable(Emoji):bool $callback - */ - public function filter(callable $callback): Dataset - { - return new self(new \CallbackFilterIterator($this, $callback)); - } - - public function indexBy(string $index = 'hexcode'): Dataset - { - if (! isset($this->indices[$index])) { - $this->indices[$index] = new self($this, $index); - } - - return $this->indices[$index]; - } - - /** - * @param string $key - */ - public function offsetGet($key): ?Emoji // phpcs:ignore - { - // Normalize shortcodes to match index. - if (\strpos($this->index, 'shortcode') !== false) { - $key = \current(Normalize::shortcodes($key)); - } - - /** @var ?Emoji $emoji */ - $emoji = parent::offsetGet($key); - - return $emoji; - } -} diff --git a/src/Dataset/Dataset.php b/src/Dataset/Dataset.php new file mode 100644 index 0000000..2bcc789 --- /dev/null +++ b/src/Dataset/Dataset.php @@ -0,0 +1,70 @@ +index = $index; + $normalized = Normalize::emojis($emojis, $index); + parent::__construct($normalized, \ArrayIterator::ARRAY_AS_PROPS | \ArrayIterator::STD_PROP_LIST); + } + + /** + * @param callable(Emoji):bool $callback + */ + public function filter(callable $callback): Dataset + { + /** @var \Iterator $iterator */ + $iterator = $this; + $filter = new \CallbackFilterIterator($iterator, $callback); + + return new self($filter); + } + + public function indexBy(string $index = 'hexcode'): Dataset + { + if (! isset($this->indices[$index])) { + $this->indices[$index] = new self($this, $index); + } + + return $this->indices[$index]; + } + + /** + * @param string $key + */ + public function offsetGet($key): ?Emoji // phpcs:ignore + { + // Normalize shortcodes to match index. + if (\strpos($this->index, 'shortcode') !== false) { + $key = (string) \current(Normalize::shortcodes($key)); + } + + /** @var ?Emoji $emoji */ + $emoji = parent::offsetGet($key); + + return $emoji; + } +} diff --git a/src/Dataset/Emoji.php b/src/Dataset/Emoji.php new file mode 100644 index 0000000..45b3907 --- /dev/null +++ b/src/Dataset/Emoji.php @@ -0,0 +1,161 @@ + '!?string', + 'emoji' => '!?string', + 'emoticon' => '!?string', + 'gender' => '?int', + 'group' => '?int', + 'hexcode' => '!?string', + 'order' => '?int', + 'shortcodes' => 'string[]<\UnicornFail\Emoji\Util\Normalize::shortcodes>', + 'skins' => '\UnicornFail\Emoji\Dataset\Dataset', + 'subgroup' => '?int', + 'tags' => 'string[]', + 'text' => '!?string', + 'tone' => 'int[]', + 'type' => 'int', + 'version' => '!?float', + ]; + + /** + * @var callable + * + * @psalm-readonly-allow-private-mutation + */ + private $renderer = '\UnicornFail\Emoji\Dataset\Emoji::renderProperty'; + + /** + * @param mixed[] $data + */ + public function __construct(array $data = []) + { + $data = Normalize::properties($data, self::PROPERTY_TYPES); + + /** @var ?string $hexcode */ + $hexcode = $data['hexcode'] ?? null; + + /** @var ?string $emoji */ + $emoji = $data['emoji'] ?? null; + + /** @var ?string $text */ + $text = $data['text'] ?? null; + + $type = (int) ($data['type'] ?? EmojibaseDatasetInterface::EMOJI); + + $data['htmlEntity'] = null; + if ($hexcode !== null) { + $data['htmlEntity'] = '&#x' . \implode(';&#x', \explode('-', $hexcode)) . ';'; + } + + $data['unicode'] = $text; + if ($type === EmojibaseDatasetInterface::EMOJI && $emoji) { + $data['unicode'] = $emoji; + } + + parent::__construct($data); + } + + public static function renderProperty(Emoji $emoji, string $property = 'unicode'): string + { + return (string) ($emoji->$property ?? ''); + } + + public function __toString(): string + { + return (string) $this->render(); + } + + /** + * @param string[]|null $exclude + */ + public function getShortcode(?array $exclude = null, bool $wrap = false): ?string + { + $shortcode = \current($this->getShortcodes($exclude)) ?: null; + + if ($shortcode !== null && $wrap) { + $shortcode = \sprintf(':%s:', $shortcode); + } + + return $shortcode; + } + + /** + * @param ?string[] $exclude + * + * @return string[] + */ + public function getShortcodes(?array $exclude = null): array + { + if ($exclude !== null) { + return \array_diff($this->shortcodes, $exclude); + } + + return $this->shortcodes; + } + + public function getSkin(int $tone = EmojibaseSkinsInterface::LIGHT_SKIN): ?self + { + /** @var ?static $skin */ + $skin = \current($this->skins->filter(static function (Emoji $emoji) use ($tone) { + return \in_array($tone, (array) $emoji->tone, true); + })->getArrayCopy()) ?: null; + + return $skin; + } + + /** {@inheritDoc} */ + public function jsonSerialize(): string + { + return (string) $this->render(); + } + + /** @return \Stringable|string */ + public function render() + { + /** @var \Stringable|string|null $rendered */ + $rendered = \call_user_func_array($this->renderer, [$this]); + + if ($rendered instanceof \Stringable) { + return $rendered; + } + + return (string) ($rendered ?? ''); + } + + public function setRenderer(callable $renderer): void + { + $this->renderer = $renderer; + } +} diff --git a/src/Dataset/RuntimeDataset.php b/src/Dataset/RuntimeDataset.php new file mode 100644 index 0000000..31f3c52 --- /dev/null +++ b/src/Dataset/RuntimeDataset.php @@ -0,0 +1,249 @@ +config = $configuration; + $this->dataset = $dataset; + } + + /** + * @param string[] $indices + * + * @return false|string + */ + public static function archive(Dataset $dataset, array $indices = EmojiParser::INDICES) + { + foreach ($indices as $index) { + $dataset->indexBy($index); + } + + $serialize = \serialize($dataset); + + return \gzencode($serialize, 9); + } + + public static function unarchive(string $filename): Dataset + { + if (! \file_exists($filename)) { + throw new FileNotFoundException($filename); + } + + if ( + ! ($contents = \file_get_contents($filename)) || + ! ($decoded = \gzdecode($contents)) + ) { + throw new UnarchiveException($filename); + } + + try { + /** @var ?Dataset $dataset */ + $dataset = \unserialize((string) $decoded, [ + 'allowed_classes' => [Dataset::class, Emoji::class], + ]); + } catch (\Throwable $throwable) { + throw new MalformedArchiveException($filename, $throwable); + } + + if (! $dataset instanceof Dataset) { + throw new MalformedArchiveException($filename); + } + + return $dataset; + } + + public function count(): int + { + return $this->getDataset()->count(); + } + + public function current(): ?Emoji + { + /** @var ?Emoji $current */ + $current = $this->getDataset()->current(); + + return $current; + } + + /** + * @param callable(Emoji):bool $callback + */ + public function filter(callable $callback): RuntimeDataset + { + return new self($this->config, $this->getDataset()->filter($callback)); + } + + public function getDataset(): Dataset + { + if ($this->dataset === null) { + /** @var \Throwable[] $throwables */ + $throwables = []; + $locale = $this->getLocale(); + $presets = $this->getPresets(); + + $remaining = $presets; + while (\count($remaining) > 0) { + $preset = \array_shift($remaining); + try { + $this->dataset = self::unarchive(\sprintf('%s/%s/%s.gz', Dataset::DIRECTORY, $locale, $preset)); + break; + } catch (\Throwable $throwable) { + $throwables[$preset] = $throwable; + } + } + + if ($this->dataset === null) { + if ($this->config->data()->has('locale')) { + $locale = (string) $this->config->data()->get('locale'); + } + + throw new LocalePresetException($locale, $throwables); + } + } + + return $this->dataset; + } + + public function getLocale(): string + { + if ($this->locale === null) { + $this->locale = (string) ($this->config->get('locale') ?? self::DEFAULT); + } + + return $this->locale; + } + + /** + * @return string[] + */ + public function getPresets(): array + { + if ($this->presets === null) { + /** @var string[] $presets */ + $presets = (array) $this->config->get('preset'); + + // Prepend the native preset if local is requires it and enabled. + if ($this->isNative()) { + \array_unshift($presets, EmojibaseShortcodeInterface::PRESET_CLDR_NATIVE); + } else { + /** @var int|false $key */ + $key = \array_search(EmojibaseShortcodeInterface::PRESET_CLDR_NATIVE, $presets, true); + + // Only remove the CLDR native preset if it's not the only one provided. + if ($key !== false && \count($presets) !== 1) { + \array_splice($presets, $key, 1); + } + } + + $this->presets = \array_filter(\array_values(\array_unique(\array_filter($presets)))); + } + + return $this->presets; + } + + public function indexBy(string $index = 'hexcode'): RuntimeDataset + { + return new self($this->config, $this->getDataset()->indexBy($index)); + } + + public function isNative(): bool + { + if ($this->native === null) { + $locale = $this->getLocale(); + $default = \in_array($locale, EmojibaseDatasetInterface::NON_LATIN_LOCALES, true); + + /** @var ?bool $native */ + $native = $this->config->get('native'); + + $this->native = $native === null + ? $default + : $native && $default; + } + + return $this->native; + } + + public function key(): string + { + return (string) $this->getDataset()->key(); + } + + public function next(): void + { + $this->getDataset()->next(); + } + + /** @param string $offset */ + public function offsetExists($offset): bool // phpcs:ignore + { + return $this->getDataset()->offsetExists($offset); + } + + /** @param string $offset */ + public function offsetGet($offset): ?Emoji // phpcs:ignore + { + return $this->getDataset()->offsetGet($offset); + } + + /** + * @param string $offset + * @param mixed $value + */ + public function offsetSet($offset, $value): void // phpcs:ignore + { + throw new \BadMethodCallException('Unable to modify immutable object.'); + } + + /** @param string $offset */ + public function offsetUnset($offset): void // phpcs:ignore + { + throw new \BadMethodCallException('Unable to modify immutable object.'); + } + + public function rewind(): void + { + $this->getDataset()->rewind(); + } + + /** @param int $position */ + public function seek($position): void // phpcs:ignore + { + $this->getDataset()->seek($position); + } + + public function valid(): bool + { + return $this->getDataset()->valid(); + } +} diff --git a/src/Emoji.php b/src/Emoji.php deleted file mode 100644 index 9b3c981..0000000 --- a/src/Emoji.php +++ /dev/null @@ -1,122 +0,0 @@ - '!?string', - 'emoji' => '!?string', - 'emoticon' => '!?string', - 'gender' => '?int', - 'group' => '?int', - 'hexcode' => '!?string', - 'order' => '?int', - 'shortcodes' => 'string[]<\UnicornFail\Emoji\Util\Normalize::shortcodes>', - 'skins' => '\UnicornFail\Emoji\Dataset', - 'subgroup' => '?int', - 'tags' => 'string[]', - 'text' => '!?string', - 'tone' => 'int[]', - 'type' => 'int', - 'version' => '!?float', - ]; - - /** - * @param mixed[] $data - */ - public function __construct(array $data = []) - { - parent::__construct(Normalize::properties($data, self::PROPERTY_TYPES)); - } - - public function __toString(): string - { - return $this->getUnicode() ?: ''; - } - - public function getHtmlEntity(): ?string - { - $hexcode = $this->hexcode; - - return $hexcode - ? '&#x' . \implode(';&#x', \explode('-', $hexcode)) . ';' - : null; - } - - /** - * @param string[]|null $exclude - */ - public function getShortcode(?array $exclude = null, bool $wrap = false): ?string - { - $shortcode = \current($this->getShortcodes($exclude)); - - if ($wrap && $shortcode) { - $shortcode = \sprintf(':%s:', $shortcode); - } - - return $shortcode ?: null; - } - - /** - * @param ?string[] $exclude - * - * @return string[] - */ - public function getShortcodes(?array $exclude = null): array - { - /** @var string[] $shortcodes */ - $shortcodes = (array) $this->offsetGet('shortcodes'); - - return $exclude - ? \array_diff($shortcodes, $exclude) - : $shortcodes; - } - - public function getSkin(int $tone = SkinsInterface::LIGHT_SKIN): ?self - { - /** @var ?static $skin */ - $skin = \current( - $this->skins->filter( - static function (Emoji $emoji) use ($tone) { - return \in_array($tone, (array) $emoji->tone, true); - } - )->getArrayCopy() - ) ?: null; - - return $skin; - } - - public function getUnicode(): ?string - { - return $this->type === DatasetInterface::EMOJI && ($emoji = $this->emoji) - ? $emoji - : $this->text; - } -} diff --git a/src/EmojiConverter.php b/src/EmojiConverter.php new file mode 100644 index 0000000..e2172ab --- /dev/null +++ b/src/EmojiConverter.php @@ -0,0 +1,73 @@ +environment = $environment; + $this->parser = $parser ?? new EmojiParser($environment); + $this->renderer = $renderer ?? new DocumentRenderer($environment); + } + + /** + * @param array $config + */ + public static function create(array $config = []): EmojiConverterInterface + { + return new self(Environment::create($config)); + } + + /** + * Converts all HTML entities, shortcodes or emoticons to emojis (unicode). + * + * @see EmojiConverterInterface::convert + * + * @throws \RuntimeException + */ + public function __invoke(string $input): string + { + return $this->convert($input); + } + + public function convert(string $input): string + { + $document = $this->parser->parse($input); + + return $this->renderer->renderDocument($document); + } + + public function getEnvironment(): EnvironmentInterface + { + return $this->environment; + } + + public function getParser(): EmojiParserInterface + { + return $this->parser; + } + + public function getRenderer(): DocumentRendererInterface + { + return $this->renderer; + } +} diff --git a/src/EmojiConverterInterface.php b/src/EmojiConverterInterface.php new file mode 100644 index 0000000..4cbf4b5 --- /dev/null +++ b/src/EmojiConverterInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji; + +use UnicornFail\Emoji\Lexer\EmojiLexer; + +/** + * Interface for a service which converts emojis. + */ +interface EmojiConverterInterface +{ + public const EMOTICON = 'emoticon'; + public const HTML_ENTITY = 'htmlEntity'; + public const SHORTCODE = 'shortcode'; + public const UNICODE = 'unicode'; + + public const TYPES = [ + EmojiLexer::T_EMOTICON => self::EMOTICON, + EmojiLexer::T_HTML_ENTITY => self::HTML_ENTITY, + EmojiLexer::T_SHORTCODE => self::SHORTCODE, + EmojiLexer::T_UNICODE => self::UNICODE, + ]; + + public function convert(string $input): string; +} diff --git a/src/Emojibase/DatasetInterface.php b/src/Emojibase/EmojibaseDatasetInterface.php similarity index 92% rename from src/Emojibase/DatasetInterface.php rename to src/Emojibase/EmojibaseDatasetInterface.php index 1f2f1e4..1d11eed 100644 --- a/src/Emojibase/DatasetInterface.php +++ b/src/Emojibase/EmojibaseDatasetInterface.php @@ -14,7 +14,7 @@ * DO NOT ATTEMPT TO DIRECTLY MODIFY THIS FILE. ALL MANUAL CHANGES MADE TO THIS FILE * WILL BE DESTROYED AUTOMATICALLY THE NEXT TIME IT IS REBUILT. */ -interface DatasetInterface +interface EmojibaseDatasetInterface { public const AUTO = null; @@ -29,9 +29,9 @@ interface DatasetInterface self::MALE => 'male', ]; - public const LATEST_CLDR_VERSION = '37'; + public const LATEST_CLDR_VERSION = '38.1'; - public const LATEST_EMOJI_VERSION = '13.0'; + public const LATEST_EMOJI_VERSION = '13.1'; public const LATEST_UNICODE_VERSION = '13.0.0'; diff --git a/src/Emojibase/GroupsInterface.php b/src/Emojibase/EmojibaseGroupsInterface.php similarity index 99% rename from src/Emojibase/GroupsInterface.php rename to src/Emojibase/EmojibaseGroupsInterface.php index ae45051..15e5642 100644 --- a/src/Emojibase/GroupsInterface.php +++ b/src/Emojibase/EmojibaseGroupsInterface.php @@ -14,7 +14,7 @@ * DO NOT ATTEMPT TO DIRECTLY MODIFY THIS FILE. ALL MANUAL CHANGES MADE TO THIS FILE * WILL BE DESTROYED AUTOMATICALLY THE NEXT TIME IT IS REBUILT. */ -interface GroupsInterface +interface EmojibaseGroupsInterface { public const GROUPS = [ self::GROUP_KEY_SMILEYS_EMOTION, diff --git a/src/Emojibase/EmojibaseRegexInterface.php b/src/Emojibase/EmojibaseRegexInterface.php new file mode 100644 index 0000000..9c673b2 --- /dev/null +++ b/src/Emojibase/EmojibaseRegexInterface.php @@ -0,0 +1,1061 @@ +|\-[#\$&\(-\*\/3<>-@B-EJ' . + 'LOPSXZ-\]cjlopsxz-\}]|[#\$&\(-\*\/3<>-@B-EJLOPSXZ-\]cjlopsxz\|\}])|[Oo][:=](?:\-[\)\]\}]|[\)\]\}])|>(?:[:' . + '=](?:\-[\(\)\/\[-\]\{\}]|[\(\)\/\[-\]\{\}])|0(?:\-[\)\]\}]|[\)\]\}]))|%(?:\-[\(\[\{]|[\(\[\{])|\\[Mm]\/|D' . + '(?:\-[:=Xx]|[:=Xx])|8(?:\-[#\)D\]\}]|[#\)D\]\}])|;(?:\-[\)P\]p\}]|[\)P\]p\}])|x(?:\-[\(D\[op\{]|[\(D\[op\\' . + '{])|X(?:\-[\(DOP\[\{]|[\(DOP\[\{])|<\/?3|[:=]\{/'; + + public const HTML_ENTITY_REGEX = '/&#x?[a-zA-Z0-9]*?;/'; + + public const SEQUENCE_REMOVAL_PATTERN = '/200D|FE0E|FE0F/g'; + + public const SHORTCODE_NATIVE_REGEX = '/:([\d+_\x{4E00}-\x{9FFF}-]|[\d+_\x{3000}-\x{30FF}-]|[\d+_\x{1100}-\x{1' . + '1FF}\x{3130}-\x{318F}\x{A960}-\x{A97F}\x{AC00}-\x{D7FF}-]|[\d+_\x{0E00}-\x{0E7F}-]|[\d+_a-z\x{0400}-\x{05' . + '2F}\x{1C80}-\x{1C8F}\x{2DE0}-\x{2DFF}\x{A640}-\x{A69F}-])+:/iu'; + + public const SHORTCODE_REGEX = '/:[\d+_a-z-]+:/'; + + public const TEXT_LOOSE_REGEX = '/\x{D83E}\x{DDD1}(?:\x{D83C}\x{DFFF}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{D83D}\\' . + 'x{DC8B}\x{200D}\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}-\x{DFFE}])|\x{D83C}\x{DFFE}\x{200D}\x{2764}\x{FE0F}\x' . + '{200D}\x{D83D}\x{DC8B}\x{200D}\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}-\x{DFFD}\x{DFFF}])|\x{D83C}\x{DFFD}\x{' . + '200D}\x{2764}\x{FE0F}\x{200D}\x{D83D}\x{DC8B}\x{200D}\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}\x{DFFC}\x{DFFE}' . + '\x{DFFF}])|\x{D83C}\x{DFFC}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{D83D}\x{DC8B}\x{200D}\x{D83E}\x{DDD1}(?:\x{' . + 'D83C}[\x{DFFB}\x{DFFD}-\x{DFFF}])|\x{D83C}\x{DFFB}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{D83D}\x{DC8B}\x{200D' . + '}\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFC}-\x{DFFF}]))|\x{D83D}\x{DC68}(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}' . + '\x{2764}\x{FE0F}\x{200D}\x{D83D}\x{DC8B}\x{200D}\x{D83D}\x{DC68}(?:\x{D83C}[\x{DFFB}-\x{DFFF}])|\x{D83D}\\' . + 'x{DC69}(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}\x{2764}\x{FE0F}\x{200D}\x{D83D}\x{DC8B}\x{200D}(?:\x{D83D}' . + '[\x{DC68}\x{DC69}])(?:\x{D83C}[\x{DFFB}-\x{DFFF}])|\x{D83D}\x{DC68}(?:\x{200D}\x{2764}\x{FE0F}\x{200D}\x{' . + 'D83D}\x{DC8B}\x{200D}\x{D83D}\x{DC68}|(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}\x{2764}\x{FE0F}\x{200D}\x{D' . + '83D}\x{DC68}(?:\x{D83C}[\x{DFFB}-\x{DFFF}]))|\x{D83E}\x{DDD1}(?:\x{D83C}\x{DFFF}\x{200D}\x{2764}\x{FE0F}\\' . + 'x{200D}\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}-\x{DFFE}])|\x{D83C}\x{DFFE}\x{200D}\x{2764}\x{FE0F}\x{200D}\x' . + '{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}-\x{DFFD}\x{DFFF}])|\x{D83C}\x{DFFD}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{' . + 'D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}\x{DFFC}\x{DFFE}\x{DFFF}])|\x{D83C}\x{DFFC}\x{200D}\x{2764}\x{FE0F}\x{20' . + '0D}\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}\x{DFFD}-\x{DFFF}])|\x{D83C}\x{DFFB}\x{200D}\x{2764}\x{FE0F}\x{200' . + 'D}\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFC}-\x{DFFF}]))|\x{D83D}\x{DC69}(?:\x{200D}\x{2764}\x{FE0F}\x{200D}\x{' . + 'D83D}\x{DC8B}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])|(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}\x{2764}\x{FE0' . + 'F}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])(?:\x{D83C}[\x{DFFB}-\x{DFFF}]))|\x{D83C}\x{DFF4}\x{DB40}\x{DC67}' . + '\x{DB40}\x{DC62}(?:\x{DB40}\x{DC77}\x{DB40}\x{DC6C}\x{DB40}\x{DC73}|\x{DB40}\x{DC73}\x{DB40}\x{DC63}\x{DB' . + '40}\x{DC74}|\x{DB40}\x{DC65}\x{DB40}\x{DC6E}\x{DB40}\x{DC67})\x{DB40}\x{DC7F}|\x{D83D}\x{DC68}\x{D83C}\x{' . + 'DFFF}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83D}\x{DC68}(?:\x{D83C}[\x{DFFB}-\x{DFFE}])|\x{D83D}\x{DC68}\x{D' . + '83C}\x{DFFE}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83D}\x{DC68}(?:\x{D83C}[\x{DFFB}-\x{DFFD}\x{DFFF}])|\x{D8' . + '3D}\x{DC68}\x{D83C}\x{DFFD}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83D}\x{DC68}(?:\x{D83C}[\x{DFFB}\x{DFFC}\x' . + '{DFFE}\x{DFFF}])|\x{D83D}\x{DC68}\x{D83C}\x{DFFC}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83D}\x{DC68}(?:\x{D8' . + '3C}[\x{DFFB}\x{DFFD}-\x{DFFF}])|\x{D83D}\x{DC68}\x{D83C}\x{DFFB}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83D}\\' . + 'x{DC68}(?:\x{D83C}[\x{DFFC}-\x{DFFF}])|\x{D83D}\x{DC69}(?:\x{200D}\x{D83D}\x{DC69}\x{200D}(?:\x{D83D}\x{D' . + 'C66}\x{200D}\x{D83D}\x{DC66}|\x{D83D}\x{DC67}\x{200D}(?:\x{D83D}[\x{DC66}\x{DC67}]))|\x{D83C}\x{DFFF}\x{2' . + '00D}\x{D83E}\x{DD1D}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])(?:\x{D83C}[\x{DFFB}-\x{DFFE}])|\x{D83C}\x{DFFE' . + '}\x{200D}\x{D83E}\x{DD1D}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])(?:\x{D83C}[\x{DFFB}-\x{DFFD}\x{DFFF}])|\x' . + '{D83C}\x{DFFD}\x{200D}\x{D83E}\x{DD1D}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])(?:\x{D83C}[\x{DFFB}\x{DFFC}\\' . + 'x{DFFE}\x{DFFF}])|\x{D83C}\x{DFFC}\x{200D}\x{D83E}\x{DD1D}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])(?:\x{D83' . + 'C}[\x{DFFB}\x{DFFD}-\x{DFFF}])|\x{D83C}\x{DFFB}\x{200D}\x{D83E}\x{DD1D}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC6' . + '9}])(?:\x{D83C}[\x{DFFC}-\x{DFFF}]))|\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}\x{D83E}\x{DD' . + '1D}\x{200D}\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}-\x{DFFF}])|\x{D83D}\x{DC68}\x{200D}(?:\x{D83D}[\x{DC68}\x' . + '{DC69}])\x{200D}(?:\x{D83D}\x{DC66}\x{200D}\x{D83D}\x{DC66}|\x{D83D}\x{DC67}\x{200D}(?:\x{D83D}[\x{DC66}\\' . + 'x{DC67}]))|\x{D83D}\x{DC68}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{D83D}\x{DC68}|\x{D83D}\x{DC69}\x{200D}\x{27' . + '64}\x{FE0F}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])|\x{D83E}\x{DDD1}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83E' . + '}\x{DDD1}|\x{D83D}\x{DC69}\x{200D}\x{D83D}\x{DC66}\x{200D}\x{D83D}\x{DC66}|\x{D83D}\x{DC68}(?:\x{200D}(?:' . + '\x{D83D}\x{DC66}\x{200D}\x{D83D}\x{DC66}|(?:\x{D83D}[\x{DC67}-\x{DC69}])\x{200D}(?:\x{D83D}[\x{DC66}\x{DC' . + '67}]))|(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}[\x{2695}\x{2696}\x{2708}]\x{FE0F})|(?:\x{D83D}\x{DC41}\x{F' . + 'E0F}\x{200D}\x{D83D}\x{DDE8}|\x{D83C}\x{DFF3}\x{FE0F}\x{200D}\x{26A7}|(?:\x{D83C}[\x{DFC3}\x{DFC4}\x{DFCA' . + '}]|\x{D83D}[\x{DC6E}\x{DC70}\x{DC71}\x{DC73}\x{DC77}\x{DC81}\x{DC82}\x{DC86}\x{DC87}\x{DE45}-\x{DE47}\x{D' . + 'E4B}\x{DE4D}\x{DE4E}\x{DEA3}\x{DEB4}-\x{DEB6}]|\x{D83E}[\x{DD26}\x{DD35}\x{DD37}-\x{DD39}\x{DD3D}\x{DD3E}' . + '\x{DDB8}\x{DDB9}\x{DDCD}-\x{DDCF}\x{DDD4}\x{DDD6}-\x{DDDD}])(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}[\x{26' . + '40}\x{2642}]|(?:\x{26F9}|\x{D83C}[\x{DFCB}\x{DFCC}]|\x{D83D}\x{DD75})(?:\x{FE0F}|\x{D83C}[\x{DFFB}-\x{DFF' . + 'F}])\x{200D}[\x{2640}\x{2642}])\x{FE0F}|(?:\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}-\x{DFFF}])|\x{D83D}\x{DC6' . + '9}(?:\x{D83C}[\x{DFFB}-\x{DFFF}]))\x{200D}[\x{2695}\x{2696}\x{2708}]\x{FE0F}|\x{D83D}\x{DC69}\x{200D}(?:\\' . + 'x{D83D}[\x{DC67}\x{DC69}])\x{200D}(?:\x{D83D}[\x{DC66}\x{DC67}])|\x{D83C}\x{DFF3}\x{FE0F}\x{200D}\x{D83C}' . + '\x{DF08}|(?:\x{D83D}\x{DE36}\x{200D}\x{D83C}\x{DF2B}|\x{D83D}\x{DC3B}\x{200D}\x{2744}|\x{D83C}\x{DFF4}\x{' . + '200D}\x{2620}|(?:\x{D83C}[\x{DFC3}\x{DFC4}\x{DFCA}]|\x{D83D}[\x{DC6E}-\x{DC71}\x{DC73}\x{DC77}\x{DC81}\x{' . + 'DC82}\x{DC86}\x{DC87}\x{DE45}-\x{DE47}\x{DE4B}\x{DE4D}\x{DE4E}\x{DEA3}\x{DEB4}-\x{DEB6}]|\x{D83E}[\x{DD26' . + '}\x{DD35}\x{DD37}-\x{DD39}\x{DD3C}-\x{DD3E}\x{DDB8}\x{DDB9}\x{DDCD}-\x{DDCF}\x{DDD4}\x{DDD6}-\x{DDDF}])\x' . + '{200D}[\x{2640}\x{2642}])\x{FE0F}|\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}(?:\x{D83C}[\x{D' . + 'F3E}\x{DF73}\x{DF7C}\x{DF84}\x{DF93}\x{DFA4}\x{DFA8}\x{DFEB}\x{DFED}]|\x{D83D}[\x{DCBB}\x{DCBC}\x{DD27}\x' . + '{DD2C}\x{DE80}\x{DE92}]|\x{D83E}[\x{DDAF}-\x{DDB3}\x{DDBC}\x{DDBD}])|\x{D83E}\x{DDD1}\x{200D}[\x{2695}\x{' . + '2696}\x{2708}]\x{FE0F}|\x{2764}\x{FE0F}\x{200D}(?:\x{D83D}\x{DD25}|\x{D83E}\x{DE79})|(?:\x{D83D}[\x{DC68}' . + '\x{DC69}])(?:(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}(?:\x{D83C}[\x{DF3E}\x{DF73}\x{DF7C}\x{DF93}\x{DFA4}\\' . + 'x{DFA8}\x{DFEB}\x{DFED}]|\x{D83D}[\x{DCBB}\x{DCBC}\x{DD27}\x{DD2C}\x{DE80}\x{DE92}]|\x{D83E}[\x{DDAF}-\x{' . + 'DDB3}\x{DDBC}\x{DDBD}])|\x{200D}[\x{2695}\x{2696}\x{2708}]\x{FE0F})|\x{D83D}\x{DE35}\x{200D}\x{D83D}\x{DC' . + 'AB}|\x{D83D}\x{DE2E}\x{200D}\x{D83D}\x{DCA8}|\x{D83D}\x{DC15}\x{200D}\x{D83E}\x{DDBA}|\x{D83D}\x{DC08}\x{' . + '200D}\x{2B1B}|\x{D83E}\x{DDD1}\x{200D}(?:\x{D83C}[\x{DF3E}\x{DF73}\x{DF7C}\x{DF84}\x{DF93}\x{DFA4}\x{DFA8' . + '}\x{DFEB}\x{DFED}]|\x{D83D}[\x{DCBB}\x{DCBC}\x{DD27}\x{DD2C}\x{DE80}\x{DE92}]|\x{D83E}[\x{DDAF}-\x{DDB3}\\' . + 'x{DDBC}\x{DDBD}])|(?:\x{D83D}[\x{DC68}\x{DC69}])\x{200D}(?:\x{D83C}[\x{DF3E}\x{DF73}\x{DF7C}\x{DF93}\x{DF' . + 'A4}\x{DFA8}\x{DFEB}\x{DFED}]|\x{D83D}[\x{DC66}\x{DC67}\x{DCBB}\x{DCBC}\x{DD27}\x{DD2C}\x{DE80}\x{DE92}]|\\' . + 'x{D83E}[\x{DDAF}-\x{DDB3}\x{DDBC}\x{DDBD}])|[#\*0-9]\x{FE0F}\x{20E3}|\x{D83C}\x{DDFD}\x{D83C}\x{DDF0}|\x{' . + 'D83C}\x{DDF6}\x{D83C}\x{DDE6}|\x{D83C}\x{DDF4}\x{D83C}\x{DDF2}|\x{D83C}\x{DDFF}(?:\x{D83C}[\x{DDE6}\x{DDF' . + '2}\x{DDFC}])|\x{D83C}\x{DDFE}(?:\x{D83C}[\x{DDEA}\x{DDF9}])|\x{D83C}\x{DDFC}(?:\x{D83C}[\x{DDEB}\x{DDF8}]' . + ')|\x{D83C}\x{DDFB}(?:\x{D83C}[\x{DDE6}\x{DDE8}\x{DDEA}\x{DDEC}\x{DDEE}\x{DDF3}\x{DDFA}])|\x{D83C}\x{DDFA}' . + '(?:\x{D83C}[\x{DDE6}\x{DDEC}\x{DDF2}\x{DDF3}\x{DDF8}\x{DDFE}\x{DDFF}])|\x{D83C}\x{DDF9}(?:\x{D83C}[\x{DDE' . + '6}\x{DDE8}\x{DDE9}\x{DDEB}-\x{DDED}\x{DDEF}-\x{DDF4}\x{DDF7}\x{DDF9}\x{DDFB}\x{DDFC}\x{DDFF}])|\x{D83C}\x' . + '{DDF8}(?:\x{D83C}[\x{DDE6}-\x{DDEA}\x{DDEC}-\x{DDF4}\x{DDF7}-\x{DDF9}\x{DDFB}\x{DDFD}-\x{DDFF}])|\x{D83C}' . + '\x{DDF7}(?:\x{D83C}[\x{DDEA}\x{DDF4}\x{DDF8}\x{DDFA}\x{DDFC}])|\x{D83C}\x{DDF5}(?:\x{D83C}[\x{DDE6}\x{DDE' . + 'A}-\x{DDED}\x{DDF0}-\x{DDF3}\x{DDF7}-\x{DDF9}\x{DDFC}\x{DDFE}])|\x{D83C}\x{DDF3}(?:\x{D83C}[\x{DDE6}\x{DD' . + 'E8}\x{DDEA}-\x{DDEC}\x{DDEE}\x{DDF1}\x{DDF4}\x{DDF5}\x{DDF7}\x{DDFA}\x{DDFF}])|\x{D83C}\x{DDF2}(?:\x{D83C' . + '}[\x{DDE6}\x{DDE8}-\x{DDED}\x{DDF0}-\x{DDFF}])|\x{D83C}\x{DDF1}(?:\x{D83C}[\x{DDE6}-\x{DDE8}\x{DDEE}\x{DD' . + 'F0}\x{DDF7}-\x{DDFB}\x{DDFE}])|\x{D83C}\x{DDF0}(?:\x{D83C}[\x{DDEA}\x{DDEC}-\x{DDEE}\x{DDF2}\x{DDF3}\x{DD' . + 'F5}\x{DDF7}\x{DDFC}\x{DDFE}\x{DDFF}])|\x{D83C}\x{DDEF}(?:\x{D83C}[\x{DDEA}\x{DDF2}\x{DDF4}\x{DDF5}])|\x{D' . + '83C}\x{DDEE}(?:\x{D83C}[\x{DDE8}-\x{DDEA}\x{DDF1}-\x{DDF4}\x{DDF6}-\x{DDF9}])|\x{D83C}\x{DDED}(?:\x{D83C}' . + '[\x{DDF0}\x{DDF2}\x{DDF3}\x{DDF7}\x{DDF9}\x{DDFA}])|\x{D83C}\x{DDEC}(?:\x{D83C}[\x{DDE6}\x{DDE7}\x{DDE9}-' . + '\x{DDEE}\x{DDF1}-\x{DDF3}\x{DDF5}-\x{DDFA}\x{DDFC}\x{DDFE}])|\x{D83C}\x{DDEB}(?:\x{D83C}[\x{DDEE}-\x{DDF0' . + '}\x{DDF2}\x{DDF4}\x{DDF7}])|\x{D83C}\x{DDEA}(?:\x{D83C}[\x{DDE6}\x{DDE8}\x{DDEA}\x{DDEC}\x{DDED}\x{DDF7}-' . + '\x{DDFA}])|\x{D83C}\x{DDE9}(?:\x{D83C}[\x{DDEA}\x{DDEC}\x{DDEF}\x{DDF0}\x{DDF2}\x{DDF4}\x{DDFF}])|\x{D83C' . + '}\x{DDE8}(?:\x{D83C}[\x{DDE6}\x{DDE8}\x{DDE9}\x{DDEB}-\x{DDEE}\x{DDF0}-\x{DDF5}\x{DDF7}\x{DDFA}-\x{DDFF}]' . + ')|\x{D83C}\x{DDE7}(?:\x{D83C}[\x{DDE6}\x{DDE7}\x{DDE9}-\x{DDEF}\x{DDF1}-\x{DDF4}\x{DDF6}-\x{DDF9}\x{DDFB}' . + '\x{DDFC}\x{DDFE}\x{DDFF}])|\x{D83C}\x{DDE6}(?:\x{D83C}[\x{DDE8}-\x{DDEC}\x{DDEE}\x{DDF1}\x{DDF2}\x{DDF4}\\' . + 'x{DDF6}-\x{DDFA}\x{DDFC}\x{DDFD}\x{DDFF}])|(?:[\x{270A}\x{270B}]|\x{D83C}[\x{DF85}\x{DFC3}\x{DFC7}]|\x{D8' . + '3D}[\x{DC43}\x{DC4A}-\x{DC4C}\x{DC4F}\x{DC50}\x{DC66}-\x{DC69}\x{DC6B}-\x{DC6E}\x{DC70}-\x{DC78}\x{DC7C}\\' . + 'x{DC81}-\x{DC83}\x{DC85}-\x{DC87}\x{DC8F}\x{DC91}\x{DCAA}\x{DD7A}\x{DD95}\x{DD96}\x{DE45}-\x{DE47}\x{DE4B' . + '}-\x{DE4F}\x{DEA3}\x{DEB4}-\x{DEB6}\x{DEC0}\x{DECC}]|\x{D83E}[\x{DD0C}\x{DD0F}\x{DD18}-\x{DD1C}\x{DD1E}\x' . + '{DD1F}\x{DD26}\x{DD30}-\x{DD39}\x{DD3D}\x{DD3E}\x{DD77}\x{DDB5}\x{DDB6}\x{DDB8}\x{DDB9}\x{DDBB}\x{DDCD}-\\' . + 'x{DDCF}\x{DDD1}-\x{DDDD}])(?:\x{D83C}[\x{DFFB}-\x{DFFF}])|(?:[\x{261D}\x{26F9}\x{270C}\x{270D}]|\x{D83C}[' . + '\x{DFC2}\x{DFC4}\x{DFCA}-\x{DFCC}]|\x{D83D}[\x{DC42}\x{DC46}-\x{DC49}\x{DC4D}\x{DC4E}\x{DD74}\x{DD75}\x{D' . + 'D90}])(?:\x{FE0E}|\x{D83C}[\x{DFFB}-\x{DFFF}])|(?:[\xA9\xAE\x{203C}\x{2049}\x{2122}\x{2139}\x{2194}-\x{21' . + '99}\x{21A9}\x{21AA}\x{231A}\x{231B}\x{2328}\x{23CF}\x{23E9}\x{23EA}\x{23ED}-\x{23EF}\x{23F1}-\x{23F3}\x{2' . + '3F8}-\x{23FA}\x{24C2}\x{25AA}\x{25AB}\x{25B6}\x{25C0}\x{25FB}-\x{25FE}\x{2600}-\x{2604}\x{260E}\x{2611}\x' . + '{2614}\x{2615}\x{2618}\x{2620}\x{2622}\x{2623}\x{2626}\x{262A}\x{262E}\x{262F}\x{2638}-\x{263A}\x{2640}\x' . + '{2642}\x{2648}-\x{2653}\x{265F}\x{2660}\x{2663}\x{2665}\x{2666}\x{2668}\x{267B}\x{267E}\x{267F}\x{2692}-\\' . + 'x{2697}\x{2699}\x{269B}\x{269C}\x{26A0}\x{26A1}\x{26A7}\x{26AA}\x{26AB}\x{26B0}\x{26B1}\x{26BD}\x{26BE}\x' . + '{26C4}\x{26C5}\x{26C8}\x{26CF}\x{26D1}\x{26D3}\x{26D4}\x{26E9}\x{26EA}\x{26F0}-\x{26F5}\x{26F7}\x{26F8}\x' . + '{26FA}\x{26FD}\x{2702}\x{2708}\x{2709}\x{270F}\x{2712}\x{2714}\x{2716}\x{271D}\x{2721}\x{2733}\x{2734}\x{' . + '2744}\x{2747}\x{2753}\x{2757}\x{2763}\x{2764}\x{27A1}\x{2934}\x{2935}\x{2B05}-\x{2B07}\x{2B1B}\x{2B1C}\x{' . + '2B50}\x{2B55}\x{3030}\x{303D}\x{3297}\x{3299}]|\x{D83C}[\x{DC04}\x{DD70}\x{DD71}\x{DD7E}\x{DD7F}\x{DE02}\\' . + 'x{DE1A}\x{DE2F}\x{DE37}\x{DF0D}-\x{DF0F}\x{DF15}\x{DF1C}\x{DF21}\x{DF24}-\x{DF2C}\x{DF36}\x{DF78}\x{DF7D}' . + '\x{DF93}\x{DF96}\x{DF97}\x{DF99}-\x{DF9B}\x{DF9E}\x{DF9F}\x{DFA7}\x{DFAC}-\x{DFAE}\x{DFC6}\x{DFCD}\x{DFCE' . + '}\x{DFD4}-\x{DFE0}\x{DFED}\x{DFF3}\x{DFF5}\x{DFF7}]|\x{D83D}[\x{DC08}\x{DC15}\x{DC1F}\x{DC26}\x{DC3F}\x{D' . + 'C41}\x{DC53}\x{DC6A}\x{DC7D}\x{DCA3}\x{DCB0}\x{DCB3}\x{DCBB}\x{DCBF}\x{DCCB}\x{DCDA}\x{DCDF}\x{DCE4}-\x{D' . + 'CE6}\x{DCEA}-\x{DCED}\x{DCF7}\x{DCF9}-\x{DCFB}\x{DCFD}\x{DD08}\x{DD0D}\x{DD12}\x{DD13}\x{DD49}\x{DD4A}\x{' . + 'DD50}-\x{DD67}\x{DD6F}\x{DD70}\x{DD73}\x{DD76}-\x{DD79}\x{DD87}\x{DD8A}-\x{DD8D}\x{DDA5}\x{DDA8}\x{DDB1}\\' . + 'x{DDB2}\x{DDBC}\x{DDC2}-\x{DDC4}\x{DDD1}-\x{DDD3}\x{DDDC}-\x{DDDE}\x{DDE1}\x{DDE3}\x{DDE8}\x{DDEF}\x{DDF3' . + '}\x{DDFA}\x{DE10}\x{DE87}\x{DE8D}\x{DE91}\x{DE94}\x{DE98}\x{DEAD}\x{DEB2}\x{DEB9}\x{DEBA}\x{DEBC}\x{DECB}' . + '\x{DECD}-\x{DECF}\x{DEE0}-\x{DEE5}\x{DEE9}\x{DEF0}\x{DEF3}])\x{FE0E}|[\xA9\xAE\x{203C}\x{2049}\x{2122}\x{' . + '2139}\x{2194}-\x{2199}\x{21A9}\x{21AA}\x{231A}\x{231B}\x{2328}\x{23CF}\x{23E9}-\x{23F3}\x{23F8}-\x{23FA}\\' . + 'x{24C2}\x{25AA}\x{25AB}\x{25B6}\x{25C0}\x{25FB}-\x{25FE}\x{2600}-\x{2604}\x{260E}\x{2611}\x{2614}\x{2615}' . + '\x{2618}\x{261D}\x{2620}\x{2622}\x{2623}\x{2626}\x{262A}\x{262E}\x{262F}\x{2638}-\x{263A}\x{2640}\x{2642}' . + '\x{2648}-\x{2653}\x{265F}\x{2660}\x{2663}\x{2665}\x{2666}\x{2668}\x{267B}\x{267E}\x{267F}\x{2692}-\x{2697' . + '}\x{2699}\x{269B}\x{269C}\x{26A0}\x{26A1}\x{26A7}\x{26AA}\x{26AB}\x{26B0}\x{26B1}\x{26BD}\x{26BE}\x{26C4}' . + '\x{26C5}\x{26C8}\x{26CE}\x{26CF}\x{26D1}\x{26D3}\x{26D4}\x{26E9}\x{26EA}\x{26F0}-\x{26F5}\x{26F7}-\x{26FA' . + '}\x{26FD}\x{2702}\x{2705}\x{2708}-\x{270D}\x{270F}\x{2712}\x{2714}\x{2716}\x{271D}\x{2721}\x{2728}\x{2733' . + '}\x{2734}\x{2744}\x{2747}\x{274C}\x{274E}\x{2753}-\x{2755}\x{2757}\x{2763}\x{2764}\x{2795}-\x{2797}\x{27A' . + '1}\x{27B0}\x{27BF}\x{2934}\x{2935}\x{2B05}-\x{2B07}\x{2B1B}\x{2B1C}\x{2B50}\x{2B55}\x{3030}\x{303D}\x{329' . + '7}\x{3299}]|\x{D83C}[\x{DC04}\x{DCCF}\x{DD70}\x{DD71}\x{DD7E}\x{DD7F}\x{DD8E}\x{DD91}-\x{DD9A}\x{DDE6}-\x' . + '{DDFF}\x{DE01}\x{DE02}\x{DE1A}\x{DE2F}\x{DE32}-\x{DE3A}\x{DE50}\x{DE51}\x{DF00}-\x{DF21}\x{DF24}-\x{DF93}' . + '\x{DF96}\x{DF97}\x{DF99}-\x{DF9B}\x{DF9E}-\x{DFF0}\x{DFF3}-\x{DFF5}\x{DFF7}-\x{DFFF}]|\x{D83D}[\x{DC00}-\\' . + 'x{DCFD}\x{DCFF}-\x{DD3D}\x{DD49}-\x{DD4E}\x{DD50}-\x{DD67}\x{DD6F}\x{DD70}\x{DD73}-\x{DD7A}\x{DD87}\x{DD8' . + 'A}-\x{DD8D}\x{DD90}\x{DD95}\x{DD96}\x{DDA4}\x{DDA5}\x{DDA8}\x{DDB1}\x{DDB2}\x{DDBC}\x{DDC2}-\x{DDC4}\x{DD' . + 'D1}-\x{DDD3}\x{DDDC}-\x{DDDE}\x{DDE1}\x{DDE3}\x{DDE8}\x{DDEF}\x{DDF3}\x{DDFA}-\x{DE4F}\x{DE80}-\x{DEC5}\x' . + '{DECB}-\x{DED2}\x{DED5}-\x{DED7}\x{DEE0}-\x{DEE5}\x{DEE9}\x{DEEB}\x{DEEC}\x{DEF0}\x{DEF3}-\x{DEFC}\x{DFE0' . + '}-\x{DFEB}]|\x{D83E}[\x{DD0C}-\x{DD3A}\x{DD3C}-\x{DD45}\x{DD47}-\x{DD78}\x{DD7A}-\x{DDCB}\x{DDCD}-\x{DDFF' . + '}\x{DE70}-\x{DE74}\x{DE78}-\x{DE7A}\x{DE80}-\x{DE86}\x{DE90}-\x{DEA8}\x{DEB0}-\x{DEB6}\x{DEC0}-\x{DEC2}\x' . + '{DED0}-\x{DED6}]/u'; + + public const TEXT_REGEX = '/(?:[\xA9\xAE\x{203C}\x{2049}\x{2122}\x{2139}\x{2194}-\x{2199}\x{21A9}\x{21AA}\x{23' . + '1A}\x{231B}\x{2328}\x{23CF}\x{23E9}\x{23EA}\x{23ED}-\x{23EF}\x{23F1}-\x{23F3}\x{23F8}-\x{23FA}\x{24C2}\x{' . + '25AA}\x{25AB}\x{25B6}\x{25C0}\x{25FB}-\x{25FE}\x{2600}-\x{2604}\x{260E}\x{2611}\x{2614}\x{2615}\x{2618}\x' . + '{261D}\x{2620}\x{2622}\x{2623}\x{2626}\x{262A}\x{262E}\x{262F}\x{2638}-\x{263A}\x{2640}\x{2642}\x{2648}-\\' . + 'x{2653}\x{265F}\x{2660}\x{2663}\x{2665}\x{2666}\x{2668}\x{267B}\x{267E}\x{267F}\x{2692}-\x{2697}\x{2699}\\' . + 'x{269B}\x{269C}\x{26A0}\x{26A1}\x{26A7}\x{26AA}\x{26AB}\x{26B0}\x{26B1}\x{26BD}\x{26BE}\x{26C4}\x{26C5}\x' . + '{26C8}\x{26CF}\x{26D1}\x{26D3}\x{26D4}\x{26E9}\x{26EA}\x{26F0}-\x{26F5}\x{26F7}-\x{26FA}\x{26FD}\x{2702}\\' . + 'x{2708}\x{2709}\x{270C}\x{270D}\x{270F}\x{2712}\x{2714}\x{2716}\x{271D}\x{2721}\x{2733}\x{2734}\x{2744}\x' . + '{2747}\x{2753}\x{2757}\x{2763}\x{2764}\x{27A1}\x{2934}\x{2935}\x{2B05}-\x{2B07}\x{2B1B}\x{2B1C}\x{2B50}\x' . + '{2B55}\x{3030}\x{303D}\x{3297}\x{3299}]|\x{D83C}[\x{DC04}\x{DD70}\x{DD71}\x{DD7E}\x{DD7F}\x{DE02}\x{DE1A}' . + '\x{DE2F}\x{DE37}\x{DF0D}-\x{DF0F}\x{DF15}\x{DF1C}\x{DF21}\x{DF24}-\x{DF2C}\x{DF36}\x{DF78}\x{DF7D}\x{DF93' . + '}\x{DF96}\x{DF97}\x{DF99}-\x{DF9B}\x{DF9E}\x{DF9F}\x{DFA7}\x{DFAC}-\x{DFAE}\x{DFC2}\x{DFC4}\x{DFC6}\x{DFC' . + 'A}-\x{DFCE}\x{DFD4}-\x{DFE0}\x{DFED}\x{DFF3}\x{DFF5}\x{DFF7}]|\x{D83D}[\x{DC08}\x{DC15}\x{DC1F}\x{DC26}\x' . + '{DC3F}\x{DC41}\x{DC42}\x{DC46}-\x{DC49}\x{DC4D}\x{DC4E}\x{DC53}\x{DC6A}\x{DC7D}\x{DCA3}\x{DCB0}\x{DCB3}\x' . + '{DCBB}\x{DCBF}\x{DCCB}\x{DCDA}\x{DCDF}\x{DCE4}-\x{DCE6}\x{DCEA}-\x{DCED}\x{DCF7}\x{DCF9}-\x{DCFB}\x{DCFD}' . + '\x{DD08}\x{DD0D}\x{DD12}\x{DD13}\x{DD49}\x{DD4A}\x{DD50}-\x{DD67}\x{DD6F}\x{DD70}\x{DD73}-\x{DD79}\x{DD87' . + '}\x{DD8A}-\x{DD8D}\x{DD90}\x{DDA5}\x{DDA8}\x{DDB1}\x{DDB2}\x{DDBC}\x{DDC2}-\x{DDC4}\x{DDD1}-\x{DDD3}\x{DD' . + 'DC}-\x{DDDE}\x{DDE1}\x{DDE3}\x{DDE8}\x{DDEF}\x{DDF3}\x{DDFA}\x{DE10}\x{DE87}\x{DE8D}\x{DE91}\x{DE94}\x{DE' . + '98}\x{DEAD}\x{DEB2}\x{DEB9}\x{DEBA}\x{DEBC}\x{DECB}\x{DECD}-\x{DECF}\x{DEE0}-\x{DEE5}\x{DEE9}\x{DEF0}\x{D' . + 'EF3}])\x{FE0E}/u'; +} diff --git a/src/Emojibase/ShortcodeInterface.php b/src/Emojibase/EmojibaseShortcodeInterface.php similarity index 98% rename from src/Emojibase/ShortcodeInterface.php rename to src/Emojibase/EmojibaseShortcodeInterface.php index 658a98f..c6d7141 100644 --- a/src/Emojibase/ShortcodeInterface.php +++ b/src/Emojibase/EmojibaseShortcodeInterface.php @@ -14,7 +14,7 @@ * DO NOT ATTEMPT TO DIRECTLY MODIFY THIS FILE. ALL MANUAL CHANGES MADE TO THIS FILE * WILL BE DESTROYED AUTOMATICALLY THE NEXT TIME IT IS REBUILT. */ -interface ShortcodeInterface +interface EmojibaseShortcodeInterface { public const DEFAULT_PRESETS = [ self::PRESET_EMOJIBASE, diff --git a/src/Emojibase/SkinsInterface.php b/src/Emojibase/EmojibaseSkinsInterface.php similarity index 97% rename from src/Emojibase/SkinsInterface.php rename to src/Emojibase/EmojibaseSkinsInterface.php index 689f8e6..39c8af9 100644 --- a/src/Emojibase/SkinsInterface.php +++ b/src/Emojibase/EmojibaseSkinsInterface.php @@ -14,7 +14,7 @@ * DO NOT ATTEMPT TO DIRECTLY MODIFY THIS FILE. ALL MANUAL CHANGES MADE TO THIS FILE * WILL BE DESTROYED AUTOMATICALLY THE NEXT TIME IT IS REBUILT. */ -interface SkinsInterface +interface EmojibaseSkinsInterface { public const DARK_SKIN = 5; diff --git a/src/Emojibase/RegexInterface.php b/src/Emojibase/RegexInterface.php deleted file mode 100644 index e64d009..0000000 --- a/src/Emojibase/RegexInterface.php +++ /dev/null @@ -1,927 +0,0 @@ -|\-[#\$&\(-\*\/3<>-@B-EJ' . - 'LOPSXZ-\]cjlopsxz-\}]|[#\$&\(-\*\/3<>-@B-EJLOPSXZ-\]cjlopsxz\|\}])|[Oo][:=](?:\-[\)\]\}]|[\)\]\}])|>(?:[:' . - '=](?:\-[\(\)\/\[-\]\{\}]|[\(\)\/\[-\]\{\}])|0(?:\-[\)\]\}]|[\)\]\}]))|%(?:\-[\(\[\{]|[\(\[\{])|\\[Mm]\/|D' . - '(?:\-[:=Xx]|[:=Xx])|8(?:\-[#\)D\]\}]|[#\)D\]\}])|;(?:\-[\)P\]p\}]|[\)P\]p\}])|x(?:\-[\(D\[op\{]|[\(D\[op\\' . - '{])|X(?:\-[\(DOP\[\{]|[\(DOP\[\{])|<\/?3|[:=]\{/'; - - public const HTML_ENTITY_REGEX = '/&#x?[a-zA-Z0-9]*?;/'; - - public const SEQUENCE_REMOVAL_PATTERN = '/200D|FE0E|FE0F/g'; - - public const SHORTCODE_NATIVE_REGEX = '/:([\d+_\x{4E00}-\x{9FFF}-]|[\d+_\x{3000}-\x{30FF}-]|[\d+_\x{1100}-\x{1' . - '1FF}\x{3130}-\x{318F}\x{A960}-\x{A97F}\x{AC00}-\x{D7FF}-]|[\d+_\x{0E00}-\x{0E7F}-]|[\d+_a-z\x{0400}-\x{05' . - '2F}\x{1C80}-\x{1C8F}\x{2DE0}-\x{2DFF}\x{A640}-\x{A69F}-])+:/i'; - - public const SHORTCODE_REGEX = '/:[\d+_a-z-]+:/'; - - public const TEXT_LOOSE_REGEX = '/\x{D83D}\x{DC68}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{D83D}\x{DC8B}\x{200D}\x{D' . - '83D}\x{DC68}|\x{D83D}\x{DC69}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{D83D}\x{DC8B}\x{200D}(?:\x{D83D}[\x{DC68}' . - '\x{DC69}])|\x{D83C}\x{DFF4}\x{DB40}\x{DC67}\x{DB40}\x{DC62}(?:\x{DB40}\x{DC77}\x{DB40}\x{DC6C}\x{DB40}\x{' . - 'DC73}|\x{DB40}\x{DC73}\x{DB40}\x{DC63}\x{DB40}\x{DC74}|\x{DB40}\x{DC65}\x{DB40}\x{DC6E}\x{DB40}\x{DC67})\\' . - 'x{DB40}\x{DC7F}|\x{D83D}\x{DC68}\x{D83C}\x{DFFF}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83D}\x{DC68}(?:\x{D83' . - 'C}[\x{DFFB}-\x{DFFE}])|\x{D83D}\x{DC68}\x{D83C}\x{DFFE}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83D}\x{DC68}(?' . - ':\x{D83C}[\x{DFFB}-\x{DFFD}\x{DFFF}])|\x{D83D}\x{DC68}\x{D83C}\x{DFFD}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{' . - 'D83D}\x{DC68}(?:\x{D83C}[\x{DFFB}\x{DFFC}\x{DFFE}\x{DFFF}])|\x{D83D}\x{DC68}\x{D83C}\x{DFFC}\x{200D}\x{D8' . - '3E}\x{DD1D}\x{200D}\x{D83D}\x{DC68}(?:\x{D83C}[\x{DFFB}\x{DFFD}-\x{DFFF}])|\x{D83D}\x{DC68}\x{D83C}\x{DFF' . - 'B}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83D}\x{DC68}(?:\x{D83C}[\x{DFFC}-\x{DFFF}])|\x{D83D}\x{DC69}(?:\x{2' . - '00D}\x{D83D}\x{DC69}\x{200D}(?:\x{D83D}\x{DC66}\x{200D}\x{D83D}\x{DC66}|\x{D83D}\x{DC67}\x{200D}(?:\x{D83' . - 'D}[\x{DC66}\x{DC67}]))|\x{D83C}\x{DFFF}\x{200D}\x{D83E}\x{DD1D}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])(?:\\' . - 'x{D83C}[\x{DFFB}-\x{DFFE}])|\x{D83C}\x{DFFE}\x{200D}\x{D83E}\x{DD1D}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}]' . - ')(?:\x{D83C}[\x{DFFB}-\x{DFFD}\x{DFFF}])|\x{D83C}\x{DFFD}\x{200D}\x{D83E}\x{DD1D}\x{200D}(?:\x{D83D}[\x{D' . - 'C68}\x{DC69}])(?:\x{D83C}[\x{DFFB}\x{DFFC}\x{DFFE}\x{DFFF}])|\x{D83C}\x{DFFC}\x{200D}\x{D83E}\x{DD1D}\x{2' . - '00D}(?:\x{D83D}[\x{DC68}\x{DC69}])(?:\x{D83C}[\x{DFFB}\x{DFFD}-\x{DFFF}])|\x{D83C}\x{DFFB}\x{200D}\x{D83E' . - '}\x{DD1D}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])(?:\x{D83C}[\x{DFFC}-\x{DFFF}]))|\x{D83E}\x{DDD1}(?:\x{D83' . - 'C}[\x{DFFB}-\x{DFFF}])\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}-\x{DFFF}])|\x{' . - 'D83D}\x{DC68}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])\x{200D}(?:\x{D83D}\x{DC66}\x{200D}\x{D83D}\x{DC66}|\x' . - '{D83D}\x{DC67}\x{200D}(?:\x{D83D}[\x{DC66}\x{DC67}]))|\x{D83D}\x{DC68}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{' . - 'D83D}\x{DC68}|\x{D83D}\x{DC69}\x{200D}\x{2764}\x{FE0F}\x{200D}(?:\x{D83D}[\x{DC68}\x{DC69}])|\x{D83E}\x{D' . - 'DD1}\x{200D}\x{D83E}\x{DD1D}\x{200D}\x{D83E}\x{DDD1}|\x{D83D}\x{DC69}\x{200D}\x{D83D}\x{DC66}\x{200D}\x{D' . - '83D}\x{DC66}|\x{D83D}\x{DC68}(?:\x{200D}(?:\x{D83D}\x{DC66}\x{200D}\x{D83D}\x{DC66}|(?:\x{D83D}[\x{DC67}-' . - '\x{DC69}])\x{200D}(?:\x{D83D}[\x{DC66}\x{DC67}]))|(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}[\x{2695}\x{2696' . - '}\x{2708}]\x{FE0F})|(?:\x{D83D}\x{DC41}\x{FE0F}\x{200D}\x{D83D}\x{DDE8}|\x{D83C}\x{DFF3}\x{FE0F}\x{200D}\\' . - 'x{26A7}|(?:\x{D83C}[\x{DFC3}\x{DFC4}\x{DFCA}]|\x{D83D}[\x{DC6E}\x{DC70}\x{DC71}\x{DC73}\x{DC77}\x{DC81}\x' . - '{DC82}\x{DC86}\x{DC87}\x{DE45}-\x{DE47}\x{DE4B}\x{DE4D}\x{DE4E}\x{DEA3}\x{DEB4}-\x{DEB6}]|\x{D83E}[\x{DD2' . - '6}\x{DD35}\x{DD37}-\x{DD39}\x{DD3D}\x{DD3E}\x{DDB8}\x{DDB9}\x{DDCD}-\x{DDCF}\x{DDD6}-\x{DDDD}])(?:\x{D83C' . - '}[\x{DFFB}-\x{DFFF}])\x{200D}[\x{2640}\x{2642}]|(?:\x{26F9}|\x{D83C}[\x{DFCB}\x{DFCC}]|\x{D83D}\x{DD75})(' . - '?:\x{FE0F}|\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}[\x{2640}\x{2642}])\x{FE0F}|(?:\x{D83E}\x{DDD1}(?:\x{D83C}' . - '[\x{DFFB}-\x{DFFF}])|\x{D83D}\x{DC69}(?:\x{D83C}[\x{DFFB}-\x{DFFF}]))\x{200D}[\x{2695}\x{2696}\x{2708}]\x' . - '{FE0F}|\x{D83D}\x{DC69}\x{200D}(?:\x{D83D}[\x{DC67}\x{DC69}])\x{200D}(?:\x{D83D}[\x{DC66}\x{DC67}])|\x{D8' . - '3C}\x{DFF3}\x{FE0F}\x{200D}\x{D83C}\x{DF08}|\x{D83E}\x{DDD1}(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}(?:\x{' . - 'D83C}[\x{DF3E}\x{DF73}\x{DF7C}\x{DF84}\x{DF93}\x{DFA4}\x{DFA8}\x{DFEB}\x{DFED}]|\x{D83D}[\x{DCBB}\x{DCBC}' . - '\x{DD27}\x{DD2C}\x{DE80}\x{DE92}]|\x{D83E}[\x{DDAF}-\x{DDB3}\x{DDBC}\x{DDBD}])|(?:\x{D83D}\x{DC3B}\x{200D' . - '}\x{2744}|\x{D83C}\x{DFF4}\x{200D}\x{2620}|(?:\x{D83C}[\x{DFC3}\x{DFC4}\x{DFCA}]|\x{D83D}[\x{DC6E}-\x{DC7' . - '1}\x{DC73}\x{DC77}\x{DC81}\x{DC82}\x{DC86}\x{DC87}\x{DE45}-\x{DE47}\x{DE4B}\x{DE4D}\x{DE4E}\x{DEA3}\x{DEB' . - '4}-\x{DEB6}]|\x{D83E}[\x{DD26}\x{DD35}\x{DD37}-\x{DD39}\x{DD3C}-\x{DD3E}\x{DDB8}\x{DDB9}\x{DDCD}-\x{DDCF}' . - '\x{DDD6}-\x{DDDF}])\x{200D}[\x{2640}\x{2642}])\x{FE0F}|\x{D83E}\x{DDD1}\x{200D}[\x{2695}\x{2696}\x{2708}]' . - '\x{FE0F}|(?:\x{D83D}[\x{DC68}\x{DC69}])(?:(?:\x{D83C}[\x{DFFB}-\x{DFFF}])\x{200D}(?:\x{D83C}[\x{DF3E}\x{D' . - 'F73}\x{DF7C}\x{DF93}\x{DFA4}\x{DFA8}\x{DFEB}\x{DFED}]|\x{D83D}[\x{DCBB}\x{DCBC}\x{DD27}\x{DD2C}\x{DE80}\x' . - '{DE92}]|\x{D83E}[\x{DDAF}-\x{DDB3}\x{DDBC}\x{DDBD}])|\x{200D}[\x{2695}\x{2696}\x{2708}]\x{FE0F})|\x{D83D}' . - '\x{DC15}\x{200D}\x{D83E}\x{DDBA}|\x{D83D}\x{DC08}\x{200D}\x{2B1B}|\x{D83E}\x{DDD1}\x{200D}(?:\x{D83C}[\x{' . - 'DF3E}\x{DF73}\x{DF7C}\x{DF84}\x{DF93}\x{DFA4}\x{DFA8}\x{DFEB}\x{DFED}]|\x{D83D}[\x{DCBB}\x{DCBC}\x{DD27}\\' . - 'x{DD2C}\x{DE80}\x{DE92}]|\x{D83E}[\x{DDAF}-\x{DDB3}\x{DDBC}\x{DDBD}])|(?:\x{D83D}[\x{DC68}\x{DC69}])\x{20' . - '0D}(?:\x{D83C}[\x{DF3E}\x{DF73}\x{DF7C}\x{DF93}\x{DFA4}\x{DFA8}\x{DFEB}\x{DFED}]|\x{D83D}[\x{DC66}\x{DC67' . - '}\x{DCBB}\x{DCBC}\x{DD27}\x{DD2C}\x{DE80}\x{DE92}]|\x{D83E}[\x{DDAF}-\x{DDB3}\x{DDBC}\x{DDBD}])|[#\*0-9]\\' . - 'x{FE0F}\x{20E3}|\x{D83C}\x{DDFD}\x{D83C}\x{DDF0}|\x{D83C}\x{DDF6}\x{D83C}\x{DDE6}|\x{D83C}\x{DDF4}\x{D83C' . - '}\x{DDF2}|\x{D83C}\x{DDFF}(?:\x{D83C}[\x{DDE6}\x{DDF2}\x{DDFC}])|\x{D83C}\x{DDFE}(?:\x{D83C}[\x{DDEA}\x{D' . - 'DF9}])|\x{D83C}\x{DDFC}(?:\x{D83C}[\x{DDEB}\x{DDF8}])|\x{D83C}\x{DDFB}(?:\x{D83C}[\x{DDE6}\x{DDE8}\x{DDEA' . - '}\x{DDEC}\x{DDEE}\x{DDF3}\x{DDFA}])|\x{D83C}\x{DDFA}(?:\x{D83C}[\x{DDE6}\x{DDEC}\x{DDF2}\x{DDF3}\x{DDF8}\\' . - 'x{DDFE}\x{DDFF}])|\x{D83C}\x{DDF9}(?:\x{D83C}[\x{DDE6}\x{DDE8}\x{DDE9}\x{DDEB}-\x{DDED}\x{DDEF}-\x{DDF4}\\' . - 'x{DDF7}\x{DDF9}\x{DDFB}\x{DDFC}\x{DDFF}])|\x{D83C}\x{DDF8}(?:\x{D83C}[\x{DDE6}-\x{DDEA}\x{DDEC}-\x{DDF4}\\' . - 'x{DDF7}-\x{DDF9}\x{DDFB}\x{DDFD}-\x{DDFF}])|\x{D83C}\x{DDF7}(?:\x{D83C}[\x{DDEA}\x{DDF4}\x{DDF8}\x{DDFA}\\' . - 'x{DDFC}])|\x{D83C}\x{DDF5}(?:\x{D83C}[\x{DDE6}\x{DDEA}-\x{DDED}\x{DDF0}-\x{DDF3}\x{DDF7}-\x{DDF9}\x{DDFC}' . - '\x{DDFE}])|\x{D83C}\x{DDF3}(?:\x{D83C}[\x{DDE6}\x{DDE8}\x{DDEA}-\x{DDEC}\x{DDEE}\x{DDF1}\x{DDF4}\x{DDF5}\\' . - 'x{DDF7}\x{DDFA}\x{DDFF}])|\x{D83C}\x{DDF2}(?:\x{D83C}[\x{DDE6}\x{DDE8}-\x{DDED}\x{DDF0}-\x{DDFF}])|\x{D83' . - 'C}\x{DDF1}(?:\x{D83C}[\x{DDE6}-\x{DDE8}\x{DDEE}\x{DDF0}\x{DDF7}-\x{DDFB}\x{DDFE}])|\x{D83C}\x{DDF0}(?:\x{' . - 'D83C}[\x{DDEA}\x{DDEC}-\x{DDEE}\x{DDF2}\x{DDF3}\x{DDF5}\x{DDF7}\x{DDFC}\x{DDFE}\x{DDFF}])|\x{D83C}\x{DDEF' . - '}(?:\x{D83C}[\x{DDEA}\x{DDF2}\x{DDF4}\x{DDF5}])|\x{D83C}\x{DDEE}(?:\x{D83C}[\x{DDE8}-\x{DDEA}\x{DDF1}-\x{' . - 'DDF4}\x{DDF6}-\x{DDF9}])|\x{D83C}\x{DDED}(?:\x{D83C}[\x{DDF0}\x{DDF2}\x{DDF3}\x{DDF7}\x{DDF9}\x{DDFA}])|\\' . - 'x{D83C}\x{DDEC}(?:\x{D83C}[\x{DDE6}\x{DDE7}\x{DDE9}-\x{DDEE}\x{DDF1}-\x{DDF3}\x{DDF5}-\x{DDFA}\x{DDFC}\x{' . - 'DDFE}])|\x{D83C}\x{DDEB}(?:\x{D83C}[\x{DDEE}-\x{DDF0}\x{DDF2}\x{DDF4}\x{DDF7}])|\x{D83C}\x{DDEA}(?:\x{D83' . - 'C}[\x{DDE6}\x{DDE8}\x{DDEA}\x{DDEC}\x{DDED}\x{DDF7}-\x{DDFA}])|\x{D83C}\x{DDE9}(?:\x{D83C}[\x{DDEA}\x{DDE' . - 'C}\x{DDEF}\x{DDF0}\x{DDF2}\x{DDF4}\x{DDFF}])|\x{D83C}\x{DDE8}(?:\x{D83C}[\x{DDE6}\x{DDE8}\x{DDE9}\x{DDEB}' . - '-\x{DDEE}\x{DDF0}-\x{DDF5}\x{DDF7}\x{DDFA}-\x{DDFF}])|\x{D83C}\x{DDE7}(?:\x{D83C}[\x{DDE6}\x{DDE7}\x{DDE9' . - '}-\x{DDEF}\x{DDF1}-\x{DDF4}\x{DDF6}-\x{DDF9}\x{DDFB}\x{DDFC}\x{DDFE}\x{DDFF}])|\x{D83C}\x{DDE6}(?:\x{D83C' . - '}[\x{DDE8}-\x{DDEC}\x{DDEE}\x{DDF1}\x{DDF2}\x{DDF4}\x{DDF6}-\x{DDFA}\x{DDFC}\x{DDFD}\x{DDFF}])|(?:[\x{270' . - 'A}\x{270B}]|\x{D83C}[\x{DF85}\x{DFC3}\x{DFC7}]|\x{D83D}[\x{DC43}\x{DC4A}-\x{DC4C}\x{DC4F}\x{DC50}\x{DC66}' . - '-\x{DC69}\x{DC6B}-\x{DC6E}\x{DC70}-\x{DC78}\x{DC7C}\x{DC81}-\x{DC83}\x{DC85}-\x{DC87}\x{DCAA}\x{DD7A}\x{D' . - 'D95}\x{DD96}\x{DE45}-\x{DE47}\x{DE4B}-\x{DE4F}\x{DEA3}\x{DEB4}-\x{DEB6}\x{DEC0}\x{DECC}]|\x{D83E}[\x{DD0C' . - '}\x{DD0F}\x{DD18}-\x{DD1C}\x{DD1E}\x{DD1F}\x{DD26}\x{DD30}-\x{DD39}\x{DD3D}\x{DD3E}\x{DD77}\x{DDB5}\x{DDB' . - '6}\x{DDB8}\x{DDB9}\x{DDBB}\x{DDCD}-\x{DDCF}\x{DDD1}-\x{DDDD}])(?:\x{D83C}[\x{DFFB}-\x{DFFF}])|(?:[\x{261D' . - '}\x{26F9}\x{270C}\x{270D}]|\x{D83C}[\x{DFC2}\x{DFC4}\x{DFCA}-\x{DFCC}]|\x{D83D}[\x{DC42}\x{DC46}-\x{DC49}' . - '\x{DC4D}\x{DC4E}\x{DD74}\x{DD75}\x{DD90}])(?:\x{FE0E}|\x{D83C}[\x{DFFB}-\x{DFFF}])|(?:[\xA9\xAE\x{203C}\x' . - '{2049}\x{2122}\x{2139}\x{2194}-\x{2199}\x{21A9}\x{21AA}\x{231A}\x{231B}\x{2328}\x{23CF}\x{23E9}\x{23EA}\x' . - '{23ED}-\x{23EF}\x{23F1}-\x{23F3}\x{23F8}-\x{23FA}\x{24C2}\x{25AA}\x{25AB}\x{25B6}\x{25C0}\x{25FB}-\x{25FE' . - '}\x{2600}-\x{2604}\x{260E}\x{2611}\x{2614}\x{2615}\x{2618}\x{2620}\x{2622}\x{2623}\x{2626}\x{262A}\x{262E' . - '}\x{262F}\x{2638}-\x{263A}\x{2640}\x{2642}\x{2648}-\x{2653}\x{265F}\x{2660}\x{2663}\x{2665}\x{2666}\x{266' . - '8}\x{267B}\x{267E}\x{267F}\x{2692}-\x{2697}\x{2699}\x{269B}\x{269C}\x{26A0}\x{26A1}\x{26A7}\x{26AA}\x{26A' . - 'B}\x{26B0}\x{26B1}\x{26BD}\x{26BE}\x{26C4}\x{26C5}\x{26C8}\x{26CF}\x{26D1}\x{26D3}\x{26D4}\x{26E9}\x{26EA' . - '}\x{26F0}-\x{26F5}\x{26F7}\x{26F8}\x{26FA}\x{26FD}\x{2702}\x{2708}\x{2709}\x{270F}\x{2712}\x{2714}\x{2716' . - '}\x{271D}\x{2721}\x{2733}\x{2734}\x{2744}\x{2747}\x{2753}\x{2757}\x{2763}\x{2764}\x{27A1}\x{2934}\x{2935}' . - '\x{2B05}-\x{2B07}\x{2B1B}\x{2B1C}\x{2B50}\x{2B55}\x{3030}\x{303D}\x{3297}\x{3299}]|\x{D83C}[\x{DC04}\x{DD' . - '70}\x{DD71}\x{DD7E}\x{DD7F}\x{DE02}\x{DE1A}\x{DE2F}\x{DE37}\x{DF0D}-\x{DF0F}\x{DF15}\x{DF1C}\x{DF21}\x{DF' . - '24}-\x{DF2C}\x{DF36}\x{DF78}\x{DF7D}\x{DF93}\x{DF96}\x{DF97}\x{DF99}-\x{DF9B}\x{DF9E}\x{DF9F}\x{DFA7}\x{D' . - 'FAC}-\x{DFAE}\x{DFC6}\x{DFCD}\x{DFCE}\x{DFD4}-\x{DFE0}\x{DFED}\x{DFF3}\x{DFF5}\x{DFF7}]|\x{D83D}[\x{DC08}' . - '\x{DC15}\x{DC1F}\x{DC26}\x{DC3F}\x{DC41}\x{DC53}\x{DC6A}\x{DC7D}\x{DCA3}\x{DCB0}\x{DCB3}\x{DCBB}\x{DCBF}\\' . - 'x{DCCB}\x{DCDA}\x{DCDF}\x{DCE4}-\x{DCE6}\x{DCEA}-\x{DCED}\x{DCF7}\x{DCF9}-\x{DCFB}\x{DCFD}\x{DD08}\x{DD0D' . - '}\x{DD12}\x{DD13}\x{DD49}\x{DD4A}\x{DD50}-\x{DD67}\x{DD6F}\x{DD70}\x{DD73}\x{DD76}-\x{DD79}\x{DD87}\x{DD8' . - 'A}-\x{DD8D}\x{DDA5}\x{DDA8}\x{DDB1}\x{DDB2}\x{DDBC}\x{DDC2}-\x{DDC4}\x{DDD1}-\x{DDD3}\x{DDDC}-\x{DDDE}\x{' . - 'DDE1}\x{DDE3}\x{DDE8}\x{DDEF}\x{DDF3}\x{DDFA}\x{DE10}\x{DE87}\x{DE8D}\x{DE91}\x{DE94}\x{DE98}\x{DEAD}\x{D' . - 'EB2}\x{DEB9}\x{DEBA}\x{DEBC}\x{DECB}\x{DECD}-\x{DECF}\x{DEE0}-\x{DEE5}\x{DEE9}\x{DEF0}\x{DEF3}])\x{FE0E}|' . - '[\xA9\xAE\x{203C}\x{2049}\x{2122}\x{2139}\x{2194}-\x{2199}\x{21A9}\x{21AA}\x{231A}\x{231B}\x{2328}\x{23CF' . - '}\x{23E9}-\x{23F3}\x{23F8}-\x{23FA}\x{24C2}\x{25AA}\x{25AB}\x{25B6}\x{25C0}\x{25FB}-\x{25FE}\x{2600}-\x{2' . - '604}\x{260E}\x{2611}\x{2614}\x{2615}\x{2618}\x{261D}\x{2620}\x{2622}\x{2623}\x{2626}\x{262A}\x{262E}\x{26' . - '2F}\x{2638}-\x{263A}\x{2640}\x{2642}\x{2648}-\x{2653}\x{265F}\x{2660}\x{2663}\x{2665}\x{2666}\x{2668}\x{2' . - '67B}\x{267E}\x{267F}\x{2692}-\x{2697}\x{2699}\x{269B}\x{269C}\x{26A0}\x{26A1}\x{26A7}\x{26AA}\x{26AB}\x{2' . - '6B0}\x{26B1}\x{26BD}\x{26BE}\x{26C4}\x{26C5}\x{26C8}\x{26CE}\x{26CF}\x{26D1}\x{26D3}\x{26D4}\x{26E9}\x{26' . - 'EA}\x{26F0}-\x{26F5}\x{26F7}-\x{26FA}\x{26FD}\x{2702}\x{2705}\x{2708}-\x{270D}\x{270F}\x{2712}\x{2714}\x{' . - '2716}\x{271D}\x{2721}\x{2728}\x{2733}\x{2734}\x{2744}\x{2747}\x{274C}\x{274E}\x{2753}-\x{2755}\x{2757}\x{' . - '2763}\x{2764}\x{2795}-\x{2797}\x{27A1}\x{27B0}\x{27BF}\x{2934}\x{2935}\x{2B05}-\x{2B07}\x{2B1B}\x{2B1C}\x' . - '{2B50}\x{2B55}\x{3030}\x{303D}\x{3297}\x{3299}]|\x{D83C}[\x{DC04}\x{DCCF}\x{DD70}\x{DD71}\x{DD7E}\x{DD7F}' . - '\x{DD8E}\x{DD91}-\x{DD9A}\x{DDE6}-\x{DDFF}\x{DE01}\x{DE02}\x{DE1A}\x{DE2F}\x{DE32}-\x{DE3A}\x{DE50}\x{DE5' . - '1}\x{DF00}-\x{DF21}\x{DF24}-\x{DF93}\x{DF96}\x{DF97}\x{DF99}-\x{DF9B}\x{DF9E}-\x{DFF0}\x{DFF3}-\x{DFF5}\x' . - '{DFF7}-\x{DFFF}]|\x{D83D}[\x{DC00}-\x{DCFD}\x{DCFF}-\x{DD3D}\x{DD49}-\x{DD4E}\x{DD50}-\x{DD67}\x{DD6F}\x{' . - 'DD70}\x{DD73}-\x{DD7A}\x{DD87}\x{DD8A}-\x{DD8D}\x{DD90}\x{DD95}\x{DD96}\x{DDA4}\x{DDA5}\x{DDA8}\x{DDB1}\x' . - '{DDB2}\x{DDBC}\x{DDC2}-\x{DDC4}\x{DDD1}-\x{DDD3}\x{DDDC}-\x{DDDE}\x{DDE1}\x{DDE3}\x{DDE8}\x{DDEF}\x{DDF3}' . - '\x{DDFA}-\x{DE4F}\x{DE80}-\x{DEC5}\x{DECB}-\x{DED2}\x{DED5}-\x{DED7}\x{DEE0}-\x{DEE5}\x{DEE9}\x{DEEB}\x{D' . - 'EEC}\x{DEF0}\x{DEF3}-\x{DEFC}\x{DFE0}-\x{DFEB}]|\x{D83E}[\x{DD0C}-\x{DD3A}\x{DD3C}-\x{DD45}\x{DD47}-\x{DD' . - '78}\x{DD7A}-\x{DDCB}\x{DDCD}-\x{DDFF}\x{DE70}-\x{DE74}\x{DE78}-\x{DE7A}\x{DE80}-\x{DE86}\x{DE90}-\x{DEA8}' . - '\x{DEB0}-\x{DEB6}\x{DEC0}-\x{DEC2}\x{DED0}-\x{DED6}]/'; - - public const TEXT_REGEX = '/(?:[\xA9\xAE\x{203C}\x{2049}\x{2122}\x{2139}\x{2194}-\x{2199}\x{21A9}\x{21AA}\x{23' . - '1A}\x{231B}\x{2328}\x{23CF}\x{23E9}\x{23EA}\x{23ED}-\x{23EF}\x{23F1}-\x{23F3}\x{23F8}-\x{23FA}\x{24C2}\x{' . - '25AA}\x{25AB}\x{25B6}\x{25C0}\x{25FB}-\x{25FE}\x{2600}-\x{2604}\x{260E}\x{2611}\x{2614}\x{2615}\x{2618}\x' . - '{261D}\x{2620}\x{2622}\x{2623}\x{2626}\x{262A}\x{262E}\x{262F}\x{2638}-\x{263A}\x{2640}\x{2642}\x{2648}-\\' . - 'x{2653}\x{265F}\x{2660}\x{2663}\x{2665}\x{2666}\x{2668}\x{267B}\x{267E}\x{267F}\x{2692}-\x{2697}\x{2699}\\' . - 'x{269B}\x{269C}\x{26A0}\x{26A1}\x{26A7}\x{26AA}\x{26AB}\x{26B0}\x{26B1}\x{26BD}\x{26BE}\x{26C4}\x{26C5}\x' . - '{26C8}\x{26CF}\x{26D1}\x{26D3}\x{26D4}\x{26E9}\x{26EA}\x{26F0}-\x{26F5}\x{26F7}-\x{26FA}\x{26FD}\x{2702}\\' . - 'x{2708}\x{2709}\x{270C}\x{270D}\x{270F}\x{2712}\x{2714}\x{2716}\x{271D}\x{2721}\x{2733}\x{2734}\x{2744}\x' . - '{2747}\x{2753}\x{2757}\x{2763}\x{2764}\x{27A1}\x{2934}\x{2935}\x{2B05}-\x{2B07}\x{2B1B}\x{2B1C}\x{2B50}\x' . - '{2B55}\x{3030}\x{303D}\x{3297}\x{3299}]|\x{D83C}[\x{DC04}\x{DD70}\x{DD71}\x{DD7E}\x{DD7F}\x{DE02}\x{DE1A}' . - '\x{DE2F}\x{DE37}\x{DF0D}-\x{DF0F}\x{DF15}\x{DF1C}\x{DF21}\x{DF24}-\x{DF2C}\x{DF36}\x{DF78}\x{DF7D}\x{DF93' . - '}\x{DF96}\x{DF97}\x{DF99}-\x{DF9B}\x{DF9E}\x{DF9F}\x{DFA7}\x{DFAC}-\x{DFAE}\x{DFC2}\x{DFC4}\x{DFC6}\x{DFC' . - 'A}-\x{DFCE}\x{DFD4}-\x{DFE0}\x{DFED}\x{DFF3}\x{DFF5}\x{DFF7}]|\x{D83D}[\x{DC08}\x{DC15}\x{DC1F}\x{DC26}\x' . - '{DC3F}\x{DC41}\x{DC42}\x{DC46}-\x{DC49}\x{DC4D}\x{DC4E}\x{DC53}\x{DC6A}\x{DC7D}\x{DCA3}\x{DCB0}\x{DCB3}\x' . - '{DCBB}\x{DCBF}\x{DCCB}\x{DCDA}\x{DCDF}\x{DCE4}-\x{DCE6}\x{DCEA}-\x{DCED}\x{DCF7}\x{DCF9}-\x{DCFB}\x{DCFD}' . - '\x{DD08}\x{DD0D}\x{DD12}\x{DD13}\x{DD49}\x{DD4A}\x{DD50}-\x{DD67}\x{DD6F}\x{DD70}\x{DD73}-\x{DD79}\x{DD87' . - '}\x{DD8A}-\x{DD8D}\x{DD90}\x{DDA5}\x{DDA8}\x{DDB1}\x{DDB2}\x{DDBC}\x{DDC2}-\x{DDC4}\x{DDD1}-\x{DDD3}\x{DD' . - 'DC}-\x{DDDE}\x{DDE1}\x{DDE3}\x{DDE8}\x{DDEF}\x{DDF3}\x{DDFA}\x{DE10}\x{DE87}\x{DE8D}\x{DE91}\x{DE94}\x{DE' . - '98}\x{DEAD}\x{DEB2}\x{DEB9}\x{DEBA}\x{DEBC}\x{DECB}\x{DECD}-\x{DECF}\x{DEE0}-\x{DEE5}\x{DEE9}\x{DEF0}\x{D' . - 'EF3}])\x{FE0E}/'; -} diff --git a/src/Environment/Environment.php b/src/Environment/Environment.php new file mode 100644 index 0000000..33051b1 --- /dev/null +++ b/src/Environment/Environment.php @@ -0,0 +1,425 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Environment; + +use League\Configuration\Configuration; +use League\Configuration\ConfigurationAwareInterface; +use League\Configuration\ConfigurationInterface; +use Nette\Schema\Expect; +use Nette\Schema\Schema; +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\EventDispatcher\StoppableEventInterface; +use UnicornFail\Emoji\Dataset\RuntimeDataset; +use UnicornFail\Emoji\EmojiConverterInterface; +use UnicornFail\Emoji\Emojibase\EmojibaseDatasetInterface; +use UnicornFail\Emoji\Emojibase\EmojibaseShortcodeInterface; +use UnicornFail\Emoji\Event\ListenerData; +use UnicornFail\Emoji\Extension\ConfigurableExtensionInterface; +use UnicornFail\Emoji\Extension\ConfigureConversionTypesInterface; +use UnicornFail\Emoji\Extension\EmojiCoreExtension; +use UnicornFail\Emoji\Extension\ExtensionInterface; +use UnicornFail\Emoji\Renderer\NodeRendererInterface; +use UnicornFail\Emoji\Util\PrioritizedList; + +final class Environment implements EnvironmentBuilderInterface +{ + /** @var Configuration */ + private $config; + + /** @var ?RuntimeDataset */ + private $dataset; + + /** @var ?EventDispatcherInterface */ + private $eventDispatcher; + + /** + * @var ExtensionInterface[] + * + * @psalm-readonly-allow-private-mutation + */ + private $extensions = []; + + /** + * @var bool + * + * @psalm-readonly-allow-private-mutation + */ + private $initialized = false; + + /** + * @var ?PrioritizedList + * + * @psalm-readonly-allow-private-mutation + */ + private $listenerData; + + /** + * @var array> + * + * @psalm-readonly-allow-private-mutation + */ + private $renderersByClass = []; + + /** + * @var ExtensionInterface[] + * + * @psalm-readonly-allow-private-mutation + */ + private $uninitializedExtensions = []; + + /** + * @param array $config + */ + public function __construct(array $config = []) + { + $this->config = new Configuration(); + $this->config->merge($config); + } + + /** + * @param array $configuration + */ + public static function create(array $configuration = []): self + { + $environment = new self($configuration); + + foreach (self::defaultExtensions() as $extension) { + $environment->addExtension($extension); + } + + return $environment; + } + + /** + * @param string|string[] $value + * + * @return string[] + */ + public static function normalizeConvert($value): array + { + if (\is_array($value)) { + return $value; + } + + return \array_fill_keys(EmojiConverterInterface::TYPES, $value); + } + + /** + * @return ExtensionInterface[] + */ + protected static function defaultExtensions(): iterable + { + return [new EmojiCoreExtension()]; + } + + public static function normalizeLocale(string $locale): string + { + /** @var string[] $normalized */ + static $normalized = []; + + // Immediately return if locale is an exact match. + if (\in_array($locale, EmojibaseDatasetInterface::SUPPORTED_LOCALES, true)) { + $normalized[$locale] = $locale; + } + + // Immediately return if this local has already been normalized. + if (isset($normalized[$locale])) { + return $normalized[$locale]; + } + + $original = $locale; + $normalized[$original] = 'en'; + + // Otherwise, see if it just needs some TLC. + $locale = \strtolower($locale); + $locale = \preg_replace('/[^a-z]/', '-', $locale) ?? $locale; + foreach ([$locale, \current(\explode('-', $locale, 2))] as $locale) { + if (\in_array($locale, EmojibaseDatasetInterface::SUPPORTED_LOCALES, true)) { + $normalized[$original] = $locale; + break; + } + } + + return $normalized[$original]; + } + + /** + * @param string|string[] $presets + * + * @return string[] + */ + public static function normalizePresets($presets): ?array + { + // Map preset aliases to their correct value. + return \array_unique(\array_filter(\array_map(static function (string $preset): string { + if (isset(EmojibaseShortcodeInterface::PRESET_ALIASES[$preset])) { + return EmojibaseShortcodeInterface::PRESET_ALIASES[$preset]; + } + + return $preset; + }, \array_values((array) $presets)))); + } + + public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface + { + $this->assertUninitialized('Failed to add event listener.'); + + if ($this->listenerData === null) { + /** @var PrioritizedList $listenerData */ + $listenerData = new PrioritizedList(); + $this->listenerData = $listenerData; + } + + $this->listenerData->add(new ListenerData($eventClass, $listener), $priority); + + $object = \is_array($listener) + ? $listener[0] + : $listener; + + if ($object instanceof EnvironmentAwareInterface) { + $object->setEnvironment($this); + } + + if ($object instanceof ConfigurationAwareInterface) { + $object->setConfiguration($this->getConfiguration()); + } + + return $this; + } + + public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface + { + $this->assertUninitialized('Failed to add extension.'); + + $this->extensions[] = $extension; + $this->uninitializedExtensions[] = $extension; + + if ($extension instanceof ConfigurableExtensionInterface) { + $extension->configureSchema($this->config, $this->config->data()); + } + + return $this; + } + + public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface + { + $this->assertUninitialized('Failed to add renderer.'); + + if (! isset($this->renderersByClass[$nodeClass])) { + /** @var PrioritizedList $renderers */ + $renderers = new PrioritizedList(); + + $this->renderersByClass[$nodeClass] = $renderers; + } + + $this->renderersByClass[$nodeClass]->add($renderer, $priority); + + if ($renderer instanceof ConfigurationAwareInterface) { + $renderer->setConfiguration($this->getConfiguration()); + } + + return $this; + } + + /** + * @throws \RuntimeException + */ + protected function assertUninitialized(string $message): void + { + if ($this->initialized) { + throw new \RuntimeException(\sprintf('%s The Environment has already been initialized.', $message)); + } + } + + /** + * {@inheritDoc} + */ + public function dispatch(object $event) + { + $this->initialize(); + + if ($this->eventDispatcher !== null) { + return $this->eventDispatcher->dispatch($event); + } + + foreach ($this->getListenersForEvent($event) as $listener) { + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + return $event; + } + + $listener($event); + } + + return $event; + } + + public function getConfiguration(): ConfigurationInterface + { + $this->initializeConfiguration(); + + return $this->config->reader(); + } + + public function getRuntimeDataset(string $index = 'hexcode'): RuntimeDataset + { + $this->initialize(); + + if ($this->dataset === null) { + $this->dataset = new RuntimeDataset($this->getConfiguration()); + } + + return $this->dataset->indexBy($index); + } + + /** + * {@inheritDoc} + * + * @return ExtensionInterface[] + */ + public function getExtensions(): iterable + { + return $this->extensions; + } + + /** + * {@inheritDoc} + * + * @return iterable + */ + public function getListenersForEvent(object $event): iterable + { + if ($this->listenerData === null) { + /** @var PrioritizedList $listenerData */ + $listenerData = new PrioritizedList(); + $this->listenerData = $listenerData; + } + + /** @var ListenerData $listenerData */ + foreach ($this->listenerData as $listenerData) { + if (! \is_a($event, $listenerData->getEvent())) { + continue; + } + + yield function (object $event) use ($listenerData): void { + $this->initialize(); + + \call_user_func($listenerData->getListener(), $event); + }; + } + } + + /** + * {@inheritDoc} + */ + public function getRenderersForClass(string $nodeClass): iterable + { + $this->initialize(); + + // If renderers are defined for this specific class, return them immediately + if (isset($this->renderersByClass[$nodeClass])) { + return $this->renderersByClass[$nodeClass]; + } + + while (\class_exists($parent = (string) ($parent ?? $nodeClass)) && ($parent = \get_parent_class($parent))) { + if (! isset($this->renderersByClass[$parent])) { + continue; + } + + // "Cache" this result to avoid future loops + return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent]; + } + + return []; + } + + protected function initialize(): void + { + if ($this->initialized) { + return; + } + + $this->initializeConfiguration(); + + $this->initializeExtensions(); + + $this->initialized = true; + } + + protected function initializeConfiguration(): void + { + $this->config->addSchema('allow_unsafe_links', Expect::bool(true)); + + $default = EmojiConverterInterface::UNICODE; + + /** @var string[] $conversionTypes */ + $conversionTypes = (array) EmojiConverterInterface::TYPES; + + foreach ($this->extensions as $extension) { + if ($extension instanceof ConfigureConversionTypesInterface) { + $extension->configureConversionTypes($default, $conversionTypes, $this->config->data()); + } + } + + $conversionTypes = \array_unique($conversionTypes); + + $structuredConversionTypes = Expect::structure(\array_combine( + EmojiConverterInterface::TYPES, + \array_map(static function (string $conversionType) use ($conversionTypes, $default): Schema { + return Expect::anyOf(false, ...$conversionTypes)->default($default)->nullable(); + }, EmojiConverterInterface::TYPES) + ))->castTo('array'); + + $this->config->addSchema('convert', Expect::anyOf($structuredConversionTypes, ...$conversionTypes) + ->default(\array_fill_keys(EmojiConverterInterface::TYPES, $default)) + ->before('\UnicornFail\Emoji\Environment\Environment::normalizeConvert')); + + $this->config->addSchema('exclude', Expect::structure([ + 'shortcodes' => Expect::arrayOf('string') + ->default([]) + ->before('\UnicornFail\Emoji\Util\Normalize::shortcodes'), + ])->castTo('array')); + + $this->config->addSchema('locale', Expect::anyOf(...EmojibaseDatasetInterface::SUPPORTED_LOCALES) + ->default('en') + ->before('\UnicornFail\Emoji\Environment\Environment::normalizeLocale')); + + $this->config->addSchema('native', Expect::bool()->nullable()); + + $this->config->addSchema('presentation', Expect::anyOf(...EmojibaseDatasetInterface::SUPPORTED_PRESENTATIONS) + ->default(EmojibaseDatasetInterface::EMOJI)); + + $this->config->addSchema('preset', Expect::anyOf(Expect::listOf(Expect::anyOf(...EmojibaseShortcodeInterface::SUPPORTED_PRESETS)), ...EmojibaseShortcodeInterface::SUPPORTED_PRESETS) + ->default(EmojibaseShortcodeInterface::DEFAULT_PRESETS) + ->before('\UnicornFail\Emoji\Environment\Environment::normalizePresets')); + } + + protected function initializeExtensions(): void + { + // Ask all extensions to register their components. + while (\count($this->uninitializedExtensions) > 0) { + foreach ($this->uninitializedExtensions as $i => $extension) { + $extension->register($this); + unset($this->uninitializedExtensions[$i]); + } + } + } + + public function setEventDispatcher(EventDispatcherInterface $dispatcher): void + { + $this->eventDispatcher = $dispatcher; + } +} diff --git a/src/Environment/EnvironmentAwareInterface.php b/src/Environment/EnvironmentAwareInterface.php new file mode 100644 index 0000000..459f2d6 --- /dev/null +++ b/src/Environment/EnvironmentAwareInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Environment; + +interface EnvironmentAwareInterface +{ + public function setEnvironment(EnvironmentInterface $environment): void; +} diff --git a/src/Environment/EnvironmentBuilderInterface.php b/src/Environment/EnvironmentBuilderInterface.php new file mode 100644 index 0000000..50a791c --- /dev/null +++ b/src/Environment/EnvironmentBuilderInterface.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Environment; + +use UnicornFail\Emoji\Extension\ExtensionInterface; +use UnicornFail\Emoji\Renderer\NodeRendererInterface; + +/** + * Interface for building the Environment with any extensions, parsers, listeners, etc. that it may need + */ +interface EnvironmentBuilderInterface extends EnvironmentInterface +{ + /** + * Registers the given event listener + * + * @param string $eventClass Fully-qualified class name of the event this listener should respond to + * @param callable $listener Listener to be executed + * @param int $priority Priority (a higher number will be executed earlier) + * + * @return static + */ + public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface; + + /** + * Registers the given extension with the Environment + * + * @return static + */ + public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface; + + /** + * Registers the given node renderer with the Environment + * + * @param string $nodeClass The fully-qualified node element class name the renderer below should handle + * @param NodeRendererInterface $renderer The renderer responsible for rendering the type of element given above + * @param int $priority Priority (a higher number will be executed earlier) + * + * @return static + */ + public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface; +} diff --git a/src/Environment/EnvironmentInterface.php b/src/Environment/EnvironmentInterface.php new file mode 100644 index 0000000..9674432 --- /dev/null +++ b/src/Environment/EnvironmentInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Environment; + +use League\Configuration\ConfigurationProviderInterface; +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\EventDispatcher\ListenerProviderInterface; +use UnicornFail\Emoji\Dataset\RuntimeDataset; +use UnicornFail\Emoji\Extension\ExtensionInterface; +use UnicornFail\Emoji\Renderer\NodeRendererInterface; + +interface EnvironmentInterface extends ConfigurationProviderInterface, EventDispatcherInterface, ListenerProviderInterface +{ + /** + * @return ExtensionInterface[] + */ + public function getExtensions(): iterable; + + /** + * @psalm-param class-string|string $nodeClass + * + * @return iterable + */ + public function getRenderersForClass(string $nodeClass): iterable; + + public function getRuntimeDataset(string $index = 'hexcode'): RuntimeDataset; + + public function setEventDispatcher(EventDispatcherInterface $dispatcher): void; +} diff --git a/src/Event/AbstractEvent.php b/src/Event/AbstractEvent.php new file mode 100644 index 0000000..757f23e --- /dev/null +++ b/src/Event/AbstractEvent.php @@ -0,0 +1,58 @@ + + * + * Original code based on the Symfony EventDispatcher "Event" contract + * - (c) 2018-2019 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Event; + +use Psr\EventDispatcher\StoppableEventInterface; + +/** + * Base class for classes containing event data. + * + * This class contains no event data. It is used by events that do not pass + * state information to an event handler when an event is raised. + * + * You can call the method stopPropagation() to abort the execution of + * further listeners in your event listener. + */ +abstract class AbstractEvent implements StoppableEventInterface +{ + /** + * @var bool + * + * @psalm-readonly-allow-private-mutation + */ + private $propagationStopped = false; + + /** + * Returns whether further event listeners should be triggered. + */ + final public function isPropagationStopped(): bool + { + return $this->propagationStopped; + } + + /** + * Stops the propagation of the event to further event listeners. + * + * If multiple event listeners are connected to the same event, no + * further event listener will be triggered once any trigger calls + * stopPropagation(). + */ + final public function stopPropagation(): void + { + $this->propagationStopped = true; + } +} diff --git a/src/Event/DocumentParsedEvent.php b/src/Event/DocumentParsedEvent.php new file mode 100644 index 0000000..518a6ea --- /dev/null +++ b/src/Event/DocumentParsedEvent.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Event; + +use UnicornFail\Emoji\Node\Document; + +/** + * Event dispatched when the document has been fully parsed + */ +final class DocumentParsedEvent extends AbstractEvent +{ + /** + * @var Document + * + * @psalm-readonly + */ + private $document; + + public function __construct(Document $document) + { + $this->document = $document; + } + + public function getDocument(): Document + { + return $this->document; + } +} diff --git a/src/Event/DocumentPreParsedEvent.php b/src/Event/DocumentPreParsedEvent.php new file mode 100644 index 0000000..991347b --- /dev/null +++ b/src/Event/DocumentPreParsedEvent.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Event; + +use UnicornFail\Emoji\Node\Document; + +/** + * Event dispatched when the document is about to be parsed + */ +final class DocumentPreParsedEvent extends AbstractEvent +{ + /** + * @var Document + * + * @psalm-readonly + */ + private $document; + + /** @var string */ + private $input; + + public function __construct(Document $document, string $input) + { + $this->document = $document; + $this->input = $input; + } + + public function getDocument(): Document + { + return $this->document; + } + + public function getInput(): string + { + return $this->input; + } + + public function replaceInput(string $input): void + { + $this->input = $input; + } +} diff --git a/src/Event/DocumentRenderedEvent.php b/src/Event/DocumentRenderedEvent.php new file mode 100644 index 0000000..7734c0c --- /dev/null +++ b/src/Event/DocumentRenderedEvent.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace UnicornFail\Emoji\Event; + +final class DocumentRenderedEvent extends AbstractEvent +{ + /** @var string */ + private $content; + + public function __construct(string $content) + { + $this->content = $content; + } + + /** + * @psalm-mutation-free + */ + public function getContent(): string + { + return $this->content; + } + + /** + * @psalm-external-mutation-free + */ + public function replaceContent(string $content): void + { + $this->content = $content; + } +} diff --git a/src/Event/ListenerData.php b/src/Event/ListenerData.php new file mode 100644 index 0000000..9c35945 --- /dev/null +++ b/src/Event/ListenerData.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Event; + +/** + * @internal + * + * @psalm-immutable + */ +final class ListenerData +{ + /** @var string */ + private $event; + + /** @var callable */ + private $listener; + + public function __construct(string $event, callable $listener) + { + $this->event = $event; + $this->listener = $listener; + } + + public function getEvent(): string + { + return $this->event; + } + + public function getListener(): callable + { + return $this->listener; + } +} diff --git a/src/Exception/InvalidConfigurationException.php b/src/Exception/InvalidConfigurationException.php deleted file mode 100644 index 5862816..0000000 --- a/src/Exception/InvalidConfigurationException.php +++ /dev/null @@ -1,9 +0,0 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Exception; + +final class UnexpectedEncodingException extends \RuntimeException implements EmojiException +{ +} diff --git a/src/Extension/ConfigurableExtensionInterface.php b/src/Extension/ConfigurableExtensionInterface.php new file mode 100644 index 0000000..4f6a3ed --- /dev/null +++ b/src/Extension/ConfigurableExtensionInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Extension; + +use Dflydev\DotAccessData\Data; +use League\Configuration\ConfigurationBuilderInterface; + +interface ConfigurableExtensionInterface extends ExtensionInterface +{ + public function configureSchema(ConfigurationBuilderInterface $builder, Data $rawConfig): void; +} diff --git a/src/Extension/ConfigureConversionTypesInterface.php b/src/Extension/ConfigureConversionTypesInterface.php new file mode 100644 index 0000000..23158d7 --- /dev/null +++ b/src/Extension/ConfigureConversionTypesInterface.php @@ -0,0 +1,13 @@ +addEventListener(DocumentParsedEvent::class, new EmojiCoreProcessor()) + ->addRenderer(Image::class, new ImageRenderer()) + ->addRenderer(Emoji::class, new EmojiRenderer()) + ->addRenderer(Text::class, new TextRenderer()); + } +} diff --git a/src/Extension/EmojiCoreProcessor.php b/src/Extension/EmojiCoreProcessor.php new file mode 100644 index 0000000..3387335 --- /dev/null +++ b/src/Extension/EmojiCoreProcessor.php @@ -0,0 +1,99 @@ +config->get('exclude/shortcodes'); + + /** @var ?int $presentation */ + $presentation = $this->config->get('presentation'); + + // Ensure emojis are set to the correct stringable type. + foreach ($e->getDocument()->getNodes() as $node) { + if (! ($node instanceof Emoji)) { + continue; + } + + $literal = null; + $type = $node->getParsedType(); + $configPath = 'convert.' . (EmojiConverterInterface::TYPES[$type] ?? ''); + + /** @var ?int $conversionType */ + $conversionType = null; + + if ($this->config->exists($configPath) && ($configType = (string) ($this->config->get($configPath) ?? ''))) { + $index = \array_search($configType, EmojiConverterInterface::TYPES, true); + if (\is_int($index)) { + $conversionType = $index; + } + } + + // If the conversion type isn't one of the core Lexer:TYPES, then do nothing. + // It should be handled by a different extension/processor. + if ($conversionType === null) { + continue; + } + + switch ($conversionType) { + case EmojiLexer::T_EMOTICON: + $literal = $node->emoticon; + break; + + case EmojiLexer::T_HTML_ENTITY: + $literal = $node->htmlEntity; + break; + + case EmojiLexer::T_SHORTCODE: + if ($shortcode = $node->getShortcode($excludedShortcodes, true)) { + $literal = $shortcode; + } + + break; + + case EmojiLexer::T_TEXT: + case EmojiLexer::T_UNICODE: + if (($presentation ?? $node->type) === EmojibaseDatasetInterface::TEXT && $node->text) { + $literal = $node->text; + } else { + $literal = $node->emoji ?? $node->unicode; + } + + break; + } + + if ($literal !== null) { + $node->setContent($literal); + } + } + } + + public function setConfiguration(ConfigurationInterface $configuration): void + { + $this->config = $configuration; + } +} diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php new file mode 100644 index 0000000..ce52316 --- /dev/null +++ b/src/Extension/ExtensionInterface.php @@ -0,0 +1,24 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Extension; + +use UnicornFail\Emoji\Environment\EnvironmentBuilderInterface; + +interface ExtensionInterface +{ + public function register(EnvironmentBuilderInterface $environment): void; +} diff --git a/src/Extension/Twemoji/TwemojiExtension.php b/src/Extension/Twemoji/TwemojiExtension.php new file mode 100644 index 0000000..4af1467 --- /dev/null +++ b/src/Extension/Twemoji/TwemojiExtension.php @@ -0,0 +1,57 @@ +setAsDefaultConversionType = $setAsDefaultConversionType; + } + + /** + * {@inheritDoc} + */ + public function configureConversionTypes(string &$default, array &$conversionTypes, Data $rawConfig): void + { + // Set the default conversion type to Twemoji. + if ($this->setAsDefaultConversionType) { + $default = TwemojiProcessor::CONVERSION_TYPE; + } + + $conversionTypes[] = TwemojiProcessor::CONVERSION_TYPE; + } + + public function configureSchema(ConfigurationBuilderInterface $builder, Data $rawConfig): void + { + $builder->addSchema('twemoji', Expect::structure([ + 'classes' => Expect::arrayOf('string')->default(['twemoji']), + 'classPrefix' => Expect::string('twemoji-')->nullable(), + 'icon' => Expect::bool(false)->nullable(), + 'inline' => Expect::bool(true), + 'size' => Expect::type('int|float|string')->nullable(), + 'type' => Expect::anyOf('png', 'svg')->default('svg'), + 'urlBase' => Expect::string('https://twemoji.maxcdn.com/v/latest'), + ])); + } + + public function register(EnvironmentBuilderInterface $environment): void + { + $environment->addEventListener(DocumentParsedEvent::class, new TwemojiProcessor()); + } +} diff --git a/src/Extension/Twemoji/TwemojiProcessor.php b/src/Extension/Twemoji/TwemojiProcessor.php new file mode 100644 index 0000000..4a63ad0 --- /dev/null +++ b/src/Extension/Twemoji/TwemojiProcessor.php @@ -0,0 +1,103 @@ +config->get('twemoji.urlBase'); + + $classPrefix = (string) $this->config->get('twemoji.classPrefix'); + + /** @var int|float|string|null $size */ + $size = $this->config->get('twemoji.size'); + + $inline = (bool) $this->config->get('twemoji.inline'); + + $type = (string) $this->config->get('twemoji.type'); + + foreach ($e->getDocument()->getNodes() as $node) { + if (! ($node instanceof Emoji) || $node->hexcode === null) { + continue; + } + + $parsedType = $node->getParsedType(); + $configPath = 'convert.' . (EmojiConverterInterface::TYPES[$parsedType] ?? ''); + $conversionType = null; + + if ($this->config->exists($configPath)) { + $conversionType = (string) ($this->config->get($configPath) ?? ''); + } + + // Only convert types that are set to "twemoji". + if ($conversionType !== self::CONVERSION_TYPE) { + continue; + } + + $url = \sprintf( + '%s/%s/%s.%s', + $urlBase, + $type === 'png' ? '72x72' : 'svg', + \strtolower($node->hexcode), + $type + ); + + $image = new Image($node->getParsedValue(), $node, $url, $node->annotation, $node->annotation); + + /** @var string[] $classes */ + $classes = (array) $this->config->get('twemoji.classes'); + $image->addClass(...$classes); + + if ($node->annotation !== null) { + $image->addClass(HtmlElement::cleanCssIdentifier($classPrefix + ? $classPrefix . $node->annotation + : $node->annotation)); + } + + // Ensure image isn't massive and relative to its surroundings by inlining it. + if ($inline && $size === null) { + $image->setAttribute('style', 'width: 1em; height: 1em; vertical-align: middle;'); + } elseif ($inline && $size !== null) { + if (! \is_string($size)) { + $size .= 'em'; + } + + $image->setAttribute('style', \sprintf('width: %s; height: %s; vertical-align: middle;', $size, $size)); + } elseif ($size !== null) { + $image->setAttribute('height', (string) $size); + $image->setAttribute('width', (string) $size); + } + + $node->replaceWith($image); + } + } + + public function setConfiguration(ConfigurationInterface $configuration): void + { + $this->config = $configuration; + } +} diff --git a/src/Lexer.php b/src/Lexer.php deleted file mode 100644 index b9a817c..0000000 --- a/src/Lexer.php +++ /dev/null @@ -1,101 +0,0 @@ - 'text', - self::T_EMOTICON => 'emoticon', - self::T_HTML_ENTITY => 'htmlEntity', - self::T_SHORTCODE => 'shortcode', - self::T_UNICODE => 'unicode', - ]; - - /** @var Configuration|ConfigurationInterface */ - private $configuration; - - public function __construct(ConfigurationInterface $configuration) - { - $this->configuration = $configuration; - } - - /** - * {@inheritDoc} - * - * @return string[] - */ - protected function getCatchablePatterns() - { - $patterns = [ - self::CODEPOINT_EMOJI_LOOSE_REGEX, - self::HTML_ENTITY_REGEX, - $this->configuration->get('native') ? self::SHORTCODE_NATIVE_REGEX : self::SHORTCODE_REGEX, - ]; - if ($this->configuration->get('convertEmoticons')) { - $patterns[] = self::EMOTICON_REGEX; - } - - // Some regex patterns from the constants include the delimiter and modifiers. Because the - // lexer joins these expressions together as an OR group (|), they must be removed. - foreach ($patterns as &$pattern) { - $pattern = \trim(\rtrim($pattern, 'imsxeADSUXJu'), '/'); - } - - return $patterns; - } - - /** - * {@inheritDoc} - * - * @return string[] - */ - protected function getNonCatchablePatterns() - { - return []; - } - - /** - * {@inheritDoc} - * - * @return int - * - * @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection - */ - protected function getType(&$value) - { - if (\preg_match(self::HTML_ENTITY_REGEX, $value)) { - return self::T_HTML_ENTITY; - } - - // @phpstan-ignore-next-line - if (\preg_match($this->configuration->get('native') ? self::SHORTCODE_NATIVE_REGEX : self::SHORTCODE_REGEX, $value)) { - return self::T_SHORTCODE; - } - - if (\preg_match(self::EMOTICON_REGEX, $value)) { - return self::T_EMOTICON; - } - - if (\preg_match(self::CODEPOINT_EMOJI_LOOSE_REGEX, $value)) { - return self::T_UNICODE; - } - - return self::T_TEXT; - } -} diff --git a/src/Lexer/EmojiLexer.php b/src/Lexer/EmojiLexer.php new file mode 100644 index 0000000..fbdb9e9 --- /dev/null +++ b/src/Lexer/EmojiLexer.php @@ -0,0 +1,137 @@ +environment = $environment; + } + + /** + * {@inheritDoc} + * + * @return string[] + */ + protected function getCatchablePatterns() + { + $config = $this->environment->getConfiguration(); + + $patterns = []; + + if ($config->get('convert.unicode')) { + $patterns[] = self::CODEPOINT_EMOJI_LOOSE_REGEX; + } + + if ($config->get('convert.htmlEntity')) { + $patterns[] = self::HTML_ENTITY_REGEX; + } + + if ($config->get('convert.shortcode')) { + $patterns[] = $this->environment->getRuntimeDataset()->isNative() + ? self::SHORTCODE_NATIVE_REGEX + : self::SHORTCODE_REGEX; + } + + if ($config->get('convert.emoticon')) { + $patterns[] = self::EMOTICON_REGEX; + } + + return static::cleanPatterns($patterns); + } + + /** + * @param string[] $patterns + * + * @return string[] + */ + protected static function cleanPatterns(array $patterns): array + { + // Some regex patterns from the constants include the delimiter and modifiers. Because the + // lexer joins these expressions together as an OR group (|), they must be removed. + foreach ($patterns as &$pattern) { + $pattern = \trim(\rtrim($pattern, 'imsxeADSUXJu'), '/'); + } + + return $patterns; + } + + /** + * {@inheritDoc} + * + * @return string[] + */ + protected function getNonCatchablePatterns() + { + return []; + } + + /** + * {@inheritDoc} + * + * @return int + * + * @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection + */ + protected function getType(&$value) + { + if (\preg_match($this->environment->getRuntimeDataset()->isNative() ? self::SHORTCODE_NATIVE_REGEX : self::SHORTCODE_REGEX, $value)) { + return self::T_SHORTCODE; + } + + if (\preg_match(self::EMOTICON_REGEX, $value)) { + return self::T_EMOTICON; + } + + if (\preg_match(self::CODEPOINT_EMOJI_LOOSE_REGEX, $value)) { + return self::T_UNICODE; + } + + if (\preg_match(self::HTML_ENTITY_REGEX, $value)) { + return self::T_HTML_ENTITY; + } + + return self::T_TEXT; + } + + /** + * {@inheritDoc} + */ + public function setInput($input): void + { + if (! \mb_check_encoding($input, 'UTF-8')) { + throw new UnexpectedEncodingException('Unexpected encoding - UTF-8 or ASCII was expected'); + } + + parent::setInput($input); + } +} diff --git a/src/Lexer/EmojiLexerInterface.php b/src/Lexer/EmojiLexerInterface.php new file mode 100644 index 0000000..ed42e78 --- /dev/null +++ b/src/Lexer/EmojiLexerInterface.php @@ -0,0 +1,11 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Node; + +class Document +{ + /** @var array */ + protected $nodes = []; + + public function appendNode(Node $node): void + { + $node->setDocument($this); + + $this->nodes[] = $node; + } + + /** + * @return Node[] + */ + public function &getNodes(): array + { + return $this->nodes; + } + + public function prependNode(Node $node): void + { + $node->setDocument($this); + + \array_unshift($this->nodes, $node); + + $this->nodes = \array_values($this->nodes); + } + + public function replaceNode(Node $oldNode, ?Node $newNode = null): void + { + $index = \array_search($oldNode, $this->nodes, true); + + if ($index === false) { + return; + } + + $replacement = []; + + if ($newNode !== null) { + $oldNode->setDocument(); + $newNode->setDocument($this); + $replacement[] = $newNode; + } + + \array_splice($this->nodes, (int) $index, 1, $replacement); + + $this->nodes = \array_values($this->nodes); + } +} diff --git a/src/Node/Emoji.php b/src/Node/Emoji.php new file mode 100644 index 0000000..cec0ad7 --- /dev/null +++ b/src/Node/Emoji.php @@ -0,0 +1,98 @@ +datasetEmoji = $emoji; + $this->parsedType = $parsedType; + $this->parsedValue = $parsedValue; + } + + /** + * @param mixed[] $arguments + * + * @return ?mixed + */ + public function __call(string $name, array $arguments) + { + /** @var callable $method */ + $method = [$this->datasetEmoji, $name]; + + return \call_user_func_array($method, $arguments); + } + + /** @return ?mixed */ + public function __get(string $name) + { + return $this->datasetEmoji->$name; + } + + public function getParsedType(): int + { + return $this->parsedType; + } + + public function getParsedValue(): string + { + return $this->parsedValue; + } + + public function getSkin(int $tone = EmojibaseSkinsInterface::LIGHT_SKIN): ?self + { + $skin = $this->datasetEmoji->getSkin($tone); + + if ($skin === null) { + return null; + } + + $property = (string) (EmojiConverter::TYPES[$this->parsedType] ?? EmojiConverter::UNICODE); + $value = (string) ($skin->$property ?? $this->parsedValue); + + return new self($this->parsedType, $value, $skin); + } +} diff --git a/src/Node/EmojiContainerInterface.php b/src/Node/EmojiContainerInterface.php new file mode 100644 index 0000000..a392632 --- /dev/null +++ b/src/Node/EmojiContainerInterface.php @@ -0,0 +1,10 @@ +emoji = $emoji; + + $this->setAttribute('src', $url); + + if ($alt !== null) { + $this->setAttribute('alt', $alt); + } + + if ($title !== null) { + $this->setAttribute('title', $title); + } + } + + public function getEmoji(): Emoji + { + return $this->emoji; + } + + public function getUrl(): string + { + return (string) $this->getAttribute('src', ''); + } + + public function setUrl(string $url): void + { + $this->setAttribute('src', $url); + } +} diff --git a/src/Node/Node.php b/src/Node/Node.php new file mode 100644 index 0000000..238f3ad --- /dev/null +++ b/src/Node/Node.php @@ -0,0 +1,136 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Node; + +use Dflydev\DotAccessData\Data; + +abstract class Node extends Data implements \Stringable +{ + /** + * @var Document|null + * + * @psalm-readonly-allow-private-mutation + */ + protected $document; + + /** @var string */ + protected $content = ''; + + /** + * @param array $data + */ + public function __construct(string $content = '', array $data = []) + { + parent::__construct(['attributes' => new Data()]); + $this->content = $content; + + $this->import($data); + } + + public function __clone() + { + $this->document = null; + } + + public function __toString(): string + { + return $this->getContent(); + } + + public function addClass(string ...$classes): void + { + $class = (string) $this->getAttribute('class', ''); + + foreach ($classes as $value) { + if ($class !== '') { + $class .= ' '; + } + + $class .= $value; + } + + if ($class) { + $this->setAttribute('class', $class); + } + } + + /** + * @param mixed $default + * + * @return mixed + */ + public function getAttribute(string $name, $default = null) + { + return $this->getAttributes()->get($name, $default); + } + + public function getAttributes(): Data + { + /** @var Data $attributes */ + $attributes = $this->get('attributes'); + + return $attributes; + } + + public function getContent(): string + { + return $this->content; + } + + public function getDocument(): ?Document + { + return $this->document; + } + + public function hasAttribute(string $name): bool + { + return $this->getAttributes()->has($name); + } + + /** + * @param mixed $value + */ + public function setAttribute(string $name, $value): void + { + $this->getAttributes()->set($name, $value); + } + + /** + * @param array $attributes + */ + public function setAttributes(array $attributes = []): void + { + $this->set('attributes', new Data($attributes)); + } + + public function setContent(string $contents): void + { + $this->content = $contents; + } + + public function setDocument(?Document $document = null): void + { + $this->document = $document; + } + + public function replaceWith(Node $replacement): void + { + if ($this->document) { + $this->document->replaceNode($this, $replacement); + } + } +} diff --git a/src/Node/Text.php b/src/Node/Text.php new file mode 100644 index 0000000..6e10f9f --- /dev/null +++ b/src/Node/Text.php @@ -0,0 +1,21 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Node; + +class Text extends Node +{ +} diff --git a/src/Parser.php b/src/Parser.php deleted file mode 100644 index 553e2c9..0000000 --- a/src/Parser.php +++ /dev/null @@ -1,154 +0,0 @@ - '\UnicornFail\Emoji\Token\Emoticon', - Lexer::T_HTML_ENTITY => 'UnicornFail\Emoji\Token\HtmlEntity', - Lexer::T_SHORTCODE => 'UnicornFail\Emoji\Token\Shortcode', - Lexer::T_UNICODE => 'UnicornFail\Emoji\Token\Unicode', - ]; - - public const T_DATASETS = [ - Lexer::T_EMOTICON => 'emoticon', - Lexer::T_HTML_ENTITY => 'htmlEntity', - Lexer::T_SHORTCODE => 'shortcodes', - Lexer::T_UNICODE => 'unicode', - ]; - - /** @var ConfigurationInterface */ - private $configuration; - - /** @var Dataset */ - private $dataset; - - /** @var Lexer */ - private $lexer; - - /** - * @param mixed[]|\Traversable $configuration - */ - public function __construct(?iterable $configuration = null, ?Dataset $dataset = null, ?Lexer $lexer = null) - { - $this->configuration = Configuration::create($configuration); - - $locale = $this->configuration->get('locale'); - \assert(\is_string($locale)); - - /** @var string[] $preset */ - $preset = $this->configuration->get('preset'); - - $this->dataset = $dataset ?? self::loadLocalePreset($locale, $preset); - $this->lexer = $lexer ?? new Lexer($this->configuration); - } - - /** - * @param string[] $presets - */ - protected static function loadLocalePreset(string $locale = 'en', array $presets = ShortcodeInterface::DEFAULT_PRESETS): Dataset - { - $throwables = []; - $presets = \array_filter($presets); - $remaining = $presets; - while (\count($remaining) > 0) { - $preset = \array_shift($remaining); - try { - return Dataset::unarchive(\sprintf('%s/%s/%s.gz', Dataset::DIRECTORY, $locale, $preset)); - } catch (\Throwable $throwable) { - $throwables[$preset] = $throwable; - } - } - - throw new LocalePresetException($locale, $throwables); - } - - public function getConfiguration(): ConfigurationInterface - { - return $this->configuration; - } - - /** - * @return TokenInterface[] - */ - public function parse(string $input): array - { - $this->lexer->setInput($input); - $this->lexer->moveNext(); - - return $this->parseTokens(); - } - - /** - * @return TokenInterface[] - */ - protected function parseTokens(): array - { - $tokens = []; - while (true) { - if (! $this->lexer->lookahead) { - break; - } - - $this->lexer->moveNext(); - - $type = (int) ($this->lexer->token['type'] ?? Lexer::T_TEXT); - $value = (string) ($this->lexer->token['value'] ?? ''); - - if ($token = $type === Lexer::T_TEXT ? $this->parseTextToken($value) : $this->parseToken($type, $value)) { - $tokens[] = $token; - } - } - - return $tokens; - } - - protected function parseToken(int $type, string $value): ?EmojiTokenInterface - { - $token = null; - - // Immediately return if not a valid type. - if (isset(self::T_DATASETS[$type]) || isset(self::T_EMOJI_TOKENS[$type])) { - $dataset = $this->dataset->indexBy(self::T_DATASETS[$type]); - - $tokenClass = self::T_EMOJI_TOKENS[$type]; - - if ($emoji = $dataset->offsetGet($value)) { - // Clone the configuration here. This is necessary so it can be passed to tokens, - // which may be rendered at a later time; when the configuration may have changed. - /** @var EmojiTokenInterface $token */ - $token = new $tokenClass($value, clone $this->configuration, $emoji); - } - } - - return $token; - } - - protected function parseTextToken(string $value): ?Text - { - $text = ''; - while (true) { - $text .= $value; - if ($this->lexer->lookahead === null || $this->lexer->lookahead['type'] !== Lexer::T_TEXT) { - break; - } - - $value = (string) ($this->lexer->lookahead['value'] ?? ''); - - $this->lexer->moveNext(); - } - - return $text - ? new Text($text) - : null; - } -} diff --git a/src/Parser/EmojiParser.php b/src/Parser/EmojiParser.php new file mode 100644 index 0000000..2808b0c --- /dev/null +++ b/src/Parser/EmojiParser.php @@ -0,0 +1,117 @@ + 'emoticon', + EmojiLexer::T_HTML_ENTITY => 'htmlEntity', + EmojiLexer::T_SHORTCODE => 'shortcodes', + EmojiLexer::T_UNICODE => 'unicode', + ]; + + /** @var EnvironmentInterface */ + private $environment; + + /** @var EmojiLexer */ + private $lexer; + + public function __construct(EnvironmentInterface $environment, ?EmojiLexer $lexer = null) + { + $this->environment = $environment; + $this->lexer = $lexer ?? new EmojiLexer($environment); + } + + public function getLexer(): EmojiLexer + { + return $this->lexer; + } + + public function parse(string $input): Document + { + $preParsedEvent = new DocumentPreParsedEvent(new Document(), $input); + $this->environment->dispatch($preParsedEvent); + + $document = $preParsedEvent->getDocument(); + $input = $preParsedEvent->getInput(); + + $this->lexer->setInput($input); + $this->lexer->moveNext(); + + while (true) { + if (! $this->lexer->lookahead) { + break; + } + + $this->lexer->moveNext(); + + /** @var array $token */ + $token = $this->lexer->token; + + $value = ''; + if (((string) $token['value']) !== '') { + $value = (string) ($token['value'] ?? ''); + } + + $type = (int) $token['type']; + + $node = null; + if ($type !== EmojiLexer::T_TEXT && \in_array($type, EmojiLexer::TYPES, true)) { + $node = $this->parseEmoji($type, $value); + } + + if ($node === null) { + $node = $this->parseText($value); + } + + $document->appendNode($node); + } + + $this->environment->dispatch(new DocumentParsedEvent($document)); + + return $document; + } + + protected function parseEmoji(int $type, string $value): ?Emoji + { + $dataset = $this->environment->getRuntimeDataset(self::INDICES[$type]); + + try { + /** @var DatasetEmoji $emoji */ + $emoji = $dataset->offsetGet($value); + + return new Emoji($type, $value, $emoji); + } catch (\OutOfRangeException $exception) { + return null; + } + } + + protected function parseText(string $value): Text + { + $text = ''; + while (true) { + $text .= $value; + if ($this->lexer->lookahead === null || $this->lexer->lookahead['type'] !== EmojiLexer::T_TEXT) { + break; + } + + $value = (string) ($this->lexer->lookahead['value'] ?? ''); + + $this->lexer->moveNext(); + } + + return new Text($text); + } +} diff --git a/src/Parser/EmojiParserInterface.php b/src/Parser/EmojiParserInterface.php new file mode 100644 index 0000000..9afc115 --- /dev/null +++ b/src/Parser/EmojiParserInterface.php @@ -0,0 +1,13 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Renderer; + +use League\Configuration\ConfigurationAwareInterface; +use UnicornFail\Emoji\Environment\EnvironmentInterface; +use UnicornFail\Emoji\Event\DocumentRenderedEvent; +use UnicornFail\Emoji\Exception\RenderNodeException; +use UnicornFail\Emoji\Node\Document; +use UnicornFail\Emoji\Node\Node; + +final class DocumentRenderer implements DocumentRendererInterface +{ + /** @var EnvironmentInterface */ + private $environment; + + public function __construct(EnvironmentInterface $environment) + { + $this->environment = $environment; + } + + public function renderDocument(Document $document): string + { + $output = ''; + + foreach ($document->getNodes() as $node) { + $output .= $this->renderNode($node); + } + + $event = new DocumentRenderedEvent($output); + + $this->environment->dispatch($event); + + return $event->getContent(); + } + + /** + * @return \Stringable|string + * + * @throws RenderNodeException + */ + private function renderNode(Node $node) + { + $renderers = $this->environment->getRenderersForClass(\get_class($node)); + + /** @var NodeRendererInterface $renderer */ + foreach ($renderers as $renderer) { + if ($renderer instanceof ConfigurationAwareInterface) { + $renderer->setConfiguration($this->environment->getConfiguration()); + } + + if (($result = $renderer->render($node)) !== null) { + return $result; + } + } + + throw new RenderNodeException(\sprintf('Unable to find corresponding renderer for node type %s', \get_class($node))); + } +} diff --git a/src/Renderer/DocumentRendererInterface.php b/src/Renderer/DocumentRendererInterface.php new file mode 100644 index 0000000..b0c5d36 --- /dev/null +++ b/src/Renderer/DocumentRendererInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Renderer; + +use UnicornFail\Emoji\Node\Document; + +/** + * Renders a parsed Document AST to rendered content (string). + */ +interface DocumentRendererInterface +{ + /** + * Renders the given Document node. + */ + public function renderDocument(Document $document): string; +} diff --git a/src/Renderer/EmojiRenderer.php b/src/Renderer/EmojiRenderer.php new file mode 100644 index 0000000..f879010 --- /dev/null +++ b/src/Renderer/EmojiRenderer.php @@ -0,0 +1,35 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Renderer; + +use UnicornFail\Emoji\Node\Emoji; +use UnicornFail\Emoji\Node\Node; + +final class EmojiRenderer implements NodeRendererInterface +{ + /** + * {@inheritdoc} + */ + public function render(Node $node) + { + if (! ($node instanceof Emoji)) { + throw new \InvalidArgumentException('Incompatible node type: ' . \get_class($node)); + } + + return $node; + } +} diff --git a/src/Renderer/ImageRenderer.php b/src/Renderer/ImageRenderer.php new file mode 100644 index 0000000..3ba47a0 --- /dev/null +++ b/src/Renderer/ImageRenderer.php @@ -0,0 +1,71 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Renderer; + +use League\Configuration\ConfigurationAwareInterface; +use League\Configuration\ConfigurationInterface; +use UnicornFail\Emoji\Node\Image; +use UnicornFail\Emoji\Node\Node; +use UnicornFail\Emoji\Util\HtmlElement; + +final class ImageRenderer implements NodeRendererInterface, ConfigurationAwareInterface +{ + public const REGEX_UNSAFE_PROTOCOL = '/^javascript:|vbscript:|file:|data:/i'; + public const REGEX_SAFE_DATA_PROTOCOL = '/^data:image\/(?:png|gif|jpeg|webp)/i'; + + /** + * @var ConfigurationInterface + * + * @psalm-readonly-allow-private-mutation + * @psalm-suppress PropertyNotSetInConstructor + */ + private $config; + + /** + * @param Node $node + * + * {@inheritDoc} + * + * @psalm-suppress MoreSpecificImplementedParamType + */ + public function render(Node $node) + { + if (! ($node instanceof Image)) { + throw new \InvalidArgumentException('Incompatible node type: ' . \get_class($node)); + } + + $allowUnsafeLinks = (bool) $this->config->get('allow_unsafe_links'); + if (! $allowUnsafeLinks && self::isLinkPotentiallyUnsafe($node->getUrl())) { + $node->setAttribute('src', ''); + } + + return new HtmlElement('img', $node->getAttributes()->export(), '', true); + } + + public function setConfiguration(ConfigurationInterface $configuration): void + { + $this->config = $configuration; + } + + /** + * @psalm-pure + */ + public static function isLinkPotentiallyUnsafe(string $url): bool + { + return \preg_match(self::REGEX_UNSAFE_PROTOCOL, $url) !== 0 && \preg_match(self::REGEX_SAFE_DATA_PROTOCOL, $url) === 0; + } +} diff --git a/src/Renderer/NodeRendererInterface.php b/src/Renderer/NodeRendererInterface.php new file mode 100644 index 0000000..2f4a1c9 --- /dev/null +++ b/src/Renderer/NodeRendererInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Renderer; + +use UnicornFail\Emoji\Node\Node; + +interface NodeRendererInterface +{ + /** + * @return \Stringable|string|null + */ + public function render(Node $node); +} diff --git a/src/Renderer/TextRenderer.php b/src/Renderer/TextRenderer.php new file mode 100644 index 0000000..4e2c605 --- /dev/null +++ b/src/Renderer/TextRenderer.php @@ -0,0 +1,35 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Renderer; + +use UnicornFail\Emoji\Node\Node; +use UnicornFail\Emoji\Node\Text; + +final class TextRenderer implements NodeRendererInterface +{ + /** + * {@inheritdoc} + */ + public function render(Node $node) + { + if (! ($node instanceof Text)) { + throw new \InvalidArgumentException('Incompatible node type: ' . \get_class($node)); + } + + return $node; + } +} diff --git a/src/Token/AbstractEmojiToken.php b/src/Token/AbstractEmojiToken.php deleted file mode 100644 index 3e9d5df..0000000 --- a/src/Token/AbstractEmojiToken.php +++ /dev/null @@ -1,84 +0,0 @@ -emoji = $emoji; - - /** @var string[] $excludedShortcodes */ - $excludedShortcodes = $configuration->get('exclude.shortcodes'); - $this->setExcludedShortcodes($excludedShortcodes); - - /** @var ?int $presentation */ - $presentation = $configuration->get('presentation'); - $this->setPresentationMode($presentation); - - $stringableType = (int) ($configuration->get('stringableType') ?? Lexer::T_UNICODE); - $this->setStringableType($stringableType); - } - - public function __toString(): string - { - $emoji = $this->getEmoji(); - switch ($this->stringableType) { - case Lexer::T_EMOTICON: - return $emoji->emoticon ?? $this->getValue(); - case Lexer::T_HTML_ENTITY: - return $emoji->htmlEntity ?? $this->getValue(); - case Lexer::T_SHORTCODE: - return $emoji->getShortcode($this->excludedShortcodes, true) ?? $this->getValue(); - } - - if (($this->presentationMode ?? $emoji->type) === DatasetInterface::TEXT) { - return $emoji->text ?? $this->getValue(); - } - - return $emoji->emoji ?? $this->getValue(); - } - - public function getEmoji(): Emoji - { - return $this->emoji; - } - - /** - * {@inheritDoc} - */ - public function setExcludedShortcodes(array $excludedShortcodes = []): void - { - $this->excludedShortcodes = $excludedShortcodes; - } - - public function setPresentationMode(?int $presentationMode = null): void - { - $this->presentationMode = $presentationMode; - } - - public function setStringableType(int $stringableType): void - { - $this->stringableType = $stringableType; - } -} diff --git a/src/Token/AbstractToken.php b/src/Token/AbstractToken.php deleted file mode 100644 index 11df8e0..0000000 --- a/src/Token/AbstractToken.php +++ /dev/null @@ -1,47 +0,0 @@ -value = $value; - } - - /** - * @param TokenInterface[] $tokens - * @param callable(TokenInterface):bool $callback - * - * @return static[] - */ - public static function filter(array $tokens, ?callable $callback = null): array - { - if (! $callback) { - /** @var callable(TokenInterface):bool $callback */ - $callback = static function (TokenInterface $token): bool { - return $token instanceof static; - }; - } - - /** @var static[] $tokens */ - $tokens = \array_filter($tokens, $callback); - - return $tokens; - } - - public function __toString(): string - { - return $this->getValue(); - } - - public function getValue(): string - { - return $this->value; - } -} diff --git a/src/Token/EmojiTokenInterface.php b/src/Token/EmojiTokenInterface.php deleted file mode 100644 index dd20c57..0000000 --- a/src/Token/EmojiTokenInterface.php +++ /dev/null @@ -1,21 +0,0 @@ - $config + */ + public static function create(array $config = [], bool $setAsDefaultConversionType = true): EmojiConverterInterface + { + $environment = Environment::create($config); + $environment->addExtension(new TwemojiExtension($setAsDefaultConversionType)); + + return new self($environment); + } +} diff --git a/src/Util/ArrayCollection.php b/src/Util/ArrayCollection.php new file mode 100644 index 0000000..9de2c5e --- /dev/null +++ b/src/Util/ArrayCollection.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Util; + +/** + * Array collection + * + * Provides a wrapper around a standard PHP array. + * + * @internal + * + * @phpstan-template T + * @phpstan-implements \IteratorAggregate + * @phpstan-implements \ArrayAccess + */ +final class ArrayCollection implements \IteratorAggregate, \Countable, \ArrayAccess +{ + /** + * @var array + * @phpstan-var array + */ + private $elements; + + /** + * Constructor + * + * @param array $elements + * + * @phpstan-param array $elements + */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + } + + /** + * @return mixed|false + * + * @phpstan-return T|false + */ + public function first() + { + return \reset($this->elements); + } + + /** + * @return mixed|false + * + * @phpstan-return T|false + */ + public function last() + { + return \end($this->elements); + } + + /** + * Retrieve an external iterator + * + * @return \ArrayIterator + * + * @phpstan-return \ArrayIterator + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->elements); + } + + /** + * Count elements of an object + * + * @return int The count as an integer. + */ + public function count(): int + { + return \count($this->elements); + } + + /** + * Whether an offset exists + * + * {@inheritDoc} + * + * @phpstan-param int $offset + */ + public function offsetExists($offset): bool + { + return \array_key_exists($offset, $this->elements); + } + + /** + * Offset to retrieve + * + * {@inheritDoc} + * + * @phpstan-param int $offset + * + * @phpstan-return T|null + */ + public function offsetGet($offset) + { + return $this->elements[$offset] ?? null; + } + + /** + * Offset to set + * + * {@inheritDoc} + * + * @phpstan-param int|null $offset + * @phpstan-param T $value + */ + public function offsetSet($offset, $value): void + { + if ($offset === null) { + $this->elements[] = $value; + } else { + $this->elements[$offset] = $value; + } + } + + /** + * Offset to unset + * + * {@inheritDoc} + * + * @phpstan-param int $offset + */ + public function offsetUnset($offset): void + { + if (! \array_key_exists($offset, $this->elements)) { + return; + } + + unset($this->elements[$offset]); + } + + /** + * Returns a subset of the array + * + * @return array + * + * @phpstan-return array + */ + public function slice(int $offset, ?int $length = null): array + { + return \array_slice($this->elements, $offset, $length, true); + } + + /** + * @return array + * + * @phpstan-return array + */ + public function toArray(): array + { + return $this->elements; + } +} diff --git a/src/Util/HtmlElement.php b/src/Util/HtmlElement.php new file mode 100644 index 0000000..d7c3d4b --- /dev/null +++ b/src/Util/HtmlElement.php @@ -0,0 +1,270 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Util; + +class HtmlElement implements \Stringable +{ + public const CSS_IDENTIFIER_FILTERS = [ + ' ' => '-', + '_' => '-', + '/' => '-', + '[' => '-', + ']' => '', + ]; + + /** @var string */ + protected $tagName; + + /** @var array */ + protected $attributes = []; + + /** @var HtmlElement|HtmlElement[]|string */ + protected $contents; + + /** @var bool */ + protected $selfClosing = false; + + /** + * @param string $tagName Name of the HTML tag + * @param array $attributes Array of attributes (values should be unescaped) + * @param HtmlElement|HtmlElement[]|string|null $contents Inner contents, pre-escaped if needed + * @param bool $selfClosing Whether the tag is self-closing + */ + public function __construct(string $tagName, array $attributes = [], $contents = '', bool $selfClosing = false) + { + $this->tagName = $tagName; + $this->selfClosing = $selfClosing; + + /** + * @var string $name + * @var string|bool $value + */ + foreach ($attributes as $name => $value) { + $this->setAttribute($name, $value); + } + + $this->setContents($contents ?? ''); + } + + /** + * Prepares a string for use as a CSS identifier (element, class, or ID name). + * + * Link below shows the syntax for valid CSS identifiers (including element + * names, classes, and IDs in selectors). + * + * @see http://www.w3.org/TR/CSS21/syndata.html#characters + * + * @param string $identifier + * The identifier to clean. + * @param string[] $filter + * An array of string replacements to use on the identifier. + * + * @return string + * The cleaned identifier. + * + * Note: shamelessly copied from + * https://github.com/drupal/core-utility/blob/6807795c25836ccdb3f50d4396c4427705b7b6ad/Html.php + */ + public static function cleanCssIdentifier(string $identifier, array $filter = self::CSS_IDENTIFIER_FILTERS): string + { + // We could also use strtr() here but its much slower than str_replace(). In + // order to keep '__' to stay '__' we first replace it with a different + // placeholder after checking that it is not defined as a filter. + $doubleUnderscoreReplacements = 0; + if (! isset($filter['__'])) { + $identifier = \str_replace('__', '##', $identifier, $doubleUnderscoreReplacements); + } + + $identifier = \str_replace(\array_keys($filter), \array_values($filter), $identifier); + // Replace temporary placeholder '##' with '__' only if the original + // $identifier contained '__'. + if ($doubleUnderscoreReplacements > 0) { + $identifier = \str_replace('##', '__', $identifier); + } + + // Valid characters in a CSS identifier are: + // - the hyphen (U+002D) + // - a-z (U+0030 - U+0039) + // - A-Z (U+0041 - U+005A) + // - the underscore (U+005F) + // - 0-9 (U+0061 - U+007A) + // - ISO 10646 characters U+00A1 and higher + // We strip out any character not in the above list. + $identifier = (string) \preg_replace('/[^\x{002D}\x{0030}-\x{0039}\x{0041}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $identifier); + + // Identifiers cannot start with a digit, two hyphens, or a hyphen followed by a digit. + $identifier = (string) \preg_replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__'], $identifier); + + return $identifier; + } + + public function addClass(string ...$classes): self + { + // Merge classes into existing classes. + $existing = \explode(' ', (string) ($this->attributes['class'] ?? '')); + $classes = \array_merge($existing, $classes); + + // Split any strings that may have multiple classes in them. + $classes = \array_map(static function (string $class) { + return \explode(' ', $class); + }, $classes); + + // Flatten classes back into a single level array. + /** @var string[] $classes */ + $classes = \array_reduce( + $classes, + /** + * @param string[] $a + * @param string[] $v + * + * @return string[] + */ + static function (array $a, array $v): array { + return \array_merge($a, $v); + }, + [] + ); + + // Filter out empty items and ensure classes are unique. + $classes = \array_filter(\array_unique($classes)); + + // Normalize the classes. + $classes = \array_map('\UnicornFail\Emoji\Util\HtmlElement::cleanCssIdentifier', \array_map('trim', $classes)); + + // Convert the array of classes back into a string. + $classes = \trim(\implode(' ', $classes)); + + $this->attributes['class'] = $classes; + + return $this; + } + + public function getTagName(): string + { + return $this->tagName; + } + + /** + * @return array + */ + public function getAllAttributes(): array + { + return $this->attributes; + } + + /** + * @return string|bool|null + */ + public function getAttribute(string $key) + { + if (! isset($this->attributes[$key])) { + return null; + } + + return $this->attributes[$key]; + } + + /** + * @param string|string[]|bool $value + */ + public function setAttribute(string $key, $value): self + { + if ($key === 'class') { + $this->attributes['class'] = ''; + if (! \is_array($value)) { + $value = [$value]; + } + + /** @var string[] $value */ + return $this->addClass(...$value); + } + + if (\is_array($value)) { + $this->attributes[$key] = \implode(' ', \array_unique($value)); + } else { + $this->attributes[$key] = $value; + } + + return $this; + } + + /** + * @return HtmlElement|HtmlElement[]|string + */ + public function getContents(bool $asString = true) + { + if (! $asString) { + return $this->contents; + } + + return $this->getContentsAsString(); + } + + /** + * Sets the inner contents of the tag (must be pre-escaped if needed) + * + * @param HtmlElement|HtmlElement[]|string|null $contents + * + * @return $this + */ + public function setContents($contents): self + { + $this->contents = $contents ?? ''; + + return $this; + } + + public function __toString(): string + { + $result = '<' . $this->tagName; + + foreach ($this->attributes as $key => $value) { + if ($value === true) { + $result .= ' ' . $key; + } elseif ($value === false) { + continue; + } else { + $result .= ' ' . $key . '="' . Xml::escape($value) . '"'; + } + } + + if ($this->contents !== '') { + $result .= '>' . $this->getContentsAsString() . 'tagName . '>'; + } elseif ($this->selfClosing && $this->tagName === 'input') { + $result .= '>'; + } elseif ($this->selfClosing) { + $result .= ' />'; + } else { + $result .= '>tagName . '>'; + } + + return $result; + } + + private function getContentsAsString(): string + { + if (\is_string($this->contents)) { + return $this->contents; + } + + if (\is_array($this->contents)) { + return \implode('', $this->contents); + } + + return (string) $this->contents; + } +} diff --git a/src/Util/Normalize.php b/src/Util/Normalize.php index 075d623..f3c8569 100644 --- a/src/Util/Normalize.php +++ b/src/Util/Normalize.php @@ -4,8 +4,7 @@ namespace UnicornFail\Emoji\Util; -use UnicornFail\Emoji\Emoji; -use UnicornFail\Emoji\Emojibase\DatasetInterface; +use UnicornFail\Emoji\Dataset\Emoji; /** * {@internal} @@ -32,31 +31,7 @@ final class Normalize * * @return Emoji[] */ - public static function dataset($emojis = [], string $index = 'hexcode', array &$dataset = []): array - { - foreach (static::emojis($emojis) as $emoji) { - /** @var string[] $keys */ - $keys = \array_filter((array) $emoji->$index); - foreach ($keys as $k) { - if (isset($dataset[$k])) { - continue; - } - - $dataset[$k] = $emoji; - - static::dataset($emoji->skins, $index, $dataset); - } - } - - return $dataset; - } - - /** - * @param mixed $emojis - * - * @return Emoji[] - */ - public static function emojis($emojis = []): array + public static function emojis($emojis = [], string $index = 'hexcode', array &$dataset = []): array { if ($emojis instanceof Emoji) { $emojis = [$emojis]; @@ -82,37 +57,21 @@ public static function emojis($emojis = []): array $normalized[] = $emoji; } - return $normalized; - } - - public static function locale(string $locale): string - { - // Immediately return if locale is an exact match. - if (\in_array($locale, DatasetInterface::SUPPORTED_LOCALES, true)) { - return $locale; - } - - // Immediately return if this local has already been normalized. - /** @var string[] $normalized */ - static $normalized = []; - if (isset($normalized[$locale])) { - return $normalized[$locale]; - } + foreach ($normalized as $emoji) { + /** @var string[] $keys */ + $keys = \array_filter((array) $emoji->$index); + foreach ($keys as $k) { + if (isset($dataset[$k])) { + continue; + } - $original = $locale; - $normalized[$original] = ''; + $dataset[$k] = $emoji; - // Otherwise, see if it just needs some TLC. - $locale = \strtolower($locale); - $locale = \preg_replace('/[^a-z]/', '-', $locale) ?? $locale; - foreach ([$locale, \current(\explode('-', $locale, 2))] as $locale) { - if (\in_array($locale, DatasetInterface::SUPPORTED_LOCALES, true)) { - $normalized[$original] = $locale; - break; + self::emojis($emoji->skins, $index, $dataset); } } - return $normalized[$original]; + return $dataset; } /** @@ -150,13 +109,13 @@ public static function setType(&$value, string $type): bool $method = self::TYPE_METHODS[$type]; /** @psalm-var string $value */ - $value = static::$method($value); + $value = self::$method($value); return true; } /** - * @param string|string[] $shortcode + * @param string|array $shortcode * * @return string[] */ @@ -167,7 +126,7 @@ public static function shortcodes($shortcode): array /** @var string|string[] $shortcodes */ foreach (\func_get_args() as $shortcodes) { $normalized = \array_merge($normalized, \array_map(static function ($shortcode) { - return \preg_replace('/[^a-z0-9-]/', '-', \strtolower(\trim((string) $shortcode, ':(){}[]'))); + return \preg_replace('/[^a-z0-9-]/', '-', \strtolower(\trim((string) $shortcode, ':(){}[]'))); }, (array) $shortcodes)); } diff --git a/src/Util/PrioritizedList.php b/src/Util/PrioritizedList.php new file mode 100644 index 0000000..6162c47 --- /dev/null +++ b/src/Util/PrioritizedList.php @@ -0,0 +1,72 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Util; + +/** + * @internal + * + * @phpstan-template T + * @phpstan-implements \IteratorAggregate + */ +final class PrioritizedList implements \IteratorAggregate +{ + /** + * @var array> + * @phpstan-var array> + */ + private $list = []; + + /** + * @var ?\Traversable + * @phpstan-var ?\Traversable + */ + private $optimized; + + /** + * @param mixed $item + * + * @phpstan-param T $item + */ + public function add($item, int $priority): void + { + $this->list[$priority][] = $item; + $this->optimized = null; + } + + /** + * @return \Traversable + * + * @phpstan-return \Traversable + */ + public function getIterator(): \Traversable + { + if ($this->optimized === null) { + \krsort($this->list); + + $sorted = []; + foreach ($this->list as $group) { + foreach ($group as $item) { + $sorted[] = $item; + } + } + + $this->optimized = new \ArrayIterator($sorted); + } + + return $this->optimized; + } +} diff --git a/src/Util/Xml.php b/src/Util/Xml.php new file mode 100644 index 0000000..bef0b7a --- /dev/null +++ b/src/Util/Xml.php @@ -0,0 +1,33 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Util; + +/** + * Utility class for handling/generating XML and HTML + * + * @psalm-immutable + */ +final class Xml +{ + /** + * @psalm-pure + */ + public static function escape(string $string): string + { + return \str_replace(['&', '<', '>', '"'], ['&', '<', '>', '"'], $string); + } +} diff --git a/tests/fixtures/emojibase.md b/tests/fixtures/emojibase.md index 65b219c..d7fde57 100644 --- a/tests/fixtures/emojibase.md +++ b/tests/fixtures/emojibase.md @@ -1,7 +1,7 @@ Sed pellentesque odio vitae fermentum, auctor porta sit. Vitae consectetur fames turpis :seat::dollar::eggplant::clock230: amet non :bar-chart::repeat::no-bell::currency-exchange::ant::pager: :repeat-one::laptop::musical-note::wedding: vulputate morbi morbi quis aliquam ultrices pulvinar congue :on::womans-hat::e-mail::chart-increasing::horse-face: blandit sem commodo vel egestas nam integer donec morbi condimentum proin quis duis -in id vitae faucibus porttitor sollicitudin a sit :nut-and-bolt::circus-tent::ferris-wheel::basketball: odio lacus, turpis lectus :mouse-face::carp-streamer::dress::party-popper::pear::dragon::slot-machine::shell: cursus +in id vitae faucibus porttitor sollicitudin a sit :nut-and-bolt::circus-tent::ferris-wheel::basketball: odio lacus, turpis lectus :mouse-face::carp-streamer::dress::party::pear::dragon::slot-machine::shell: cursus :rabbit::clock230::mailbox-with-no-mail::speech-balloon::couple-with-heart::nut-and-bolt::knife: :gem::revolving-hearts::waxing-gibbous-moon::ox: suspendisse netus erat laoreet eget dui euismod nibh imperdiet sagittis varius sapien etiam eu :doughnut::palm-tree::boot::chart::hospital: consectetur sit aliquam cras maecenas. Vitae suspendisse aliquam :sunrise-over-mountains::person-surfing::pill::house-with-garden::whale::computer-disk: eget nunc :couple::camera::mute::books::small-red-triangle-down: mi -morbi non :clock7::sparkling-heart::revolving-hearts::steaming-bowl: ullamcorper eros, :anger::cooked-rice:๐Ÿ•€:ram::closed-book::ear: metus, id euismod :icecream::postbox::whale::rat::older-man::post-office:. +morbi non :clock7::sparkling-heart::revolving-hearts::ramen: ullamcorper eros, :anger::cooked-rice:๐Ÿ•€:ram::closed-book::ear: metus, id euismod :icecream::postbox::whale::rat::older-man::post-office:. diff --git a/tests/fixtures/iamcal.md b/tests/fixtures/iamcal.md index 2a1d9c7..597e447 100644 --- a/tests/fixtures/iamcal.md +++ b/tests/fixtures/iamcal.md @@ -3,5 +3,5 @@ Vitae consectetur fames turpis :seat::dollar::eggplant::clock230: amet non :bar- pulvinar congue :on::womans-hat::e-mail::chart-with-upwards-trend::horse: blandit sem commodo vel egestas nam integer donec morbi condimentum proin quis duis in id vitae faucibus porttitor sollicitudin a sit :nut-and-bolt::circus-tent::ferris-wheel::basketball: odio lacus, turpis lectus :mouse::flags::dress::tada::pear::dragon::slot-machine::shell: cursus :rabbit2::clock230::mailbox-with-no-mail::speech-balloon::couple-with-heart::nut-and-bolt::hocho: :gem::revolving-hearts::moon::ox: suspendisse netus erat laoreet eget dui euismod nibh imperdiet sagittis varius sapien etiam -eu :doughnut::palm-tree::boot::chart::hospital: consectetur sit aliquam cras maecenas. Vitae suspendisse aliquam :sunrise-over-mountains::surfer::pill::house-with-garden::whale2::minidisc: eget nunc :couple::camera::mute::books::small-red-triangle-down: mi +eu :doughnut::palm-tree::boot::chart::hospital: consectetur sit aliquam cras maecenas. Vitae suspendisse aliquam :sunrise-over-mountains::surfer::pill::house-with-garden::whale2::minidisc: eget nunc :man-and-woman-holding-hands::camera::mute::books::small-red-triangle-down: mi morbi non :clock7::sparkling-heart::revolving-hearts::ramen: ullamcorper eros, :anger::rice:๐Ÿ•€:ram::closed-book::ear: metus, id euismod :icecream::postbox::whale2::rat::older-man::post-office:. diff --git a/tests/unit/ConfigurationTest.php b/tests/unit/ConfigurationTest.php deleted file mode 100644 index 55a1205..0000000 --- a/tests/unit/ConfigurationTest.php +++ /dev/null @@ -1,231 +0,0 @@ -formatValue($value)); - if (isset($allowedValues) && \count($allowedValues) > 0) { - $message .= \sprintf(' Accepted values are: %s.', $this->formatValues($allowedValues)); - } - - return new InvalidConfigurationException($message); - } - - /** - * @param mixed $value - * @param mixed[]|null $allowedValues - */ - protected function expectInvalidOptionException(string $name, $value, ?array $allowedValues = null): void - { - $this->expectExceptionObject($this->createInvalidOptionException($name, $value, $allowedValues)); - } - - /** - * @param mixed $value - */ - private function formatValue($value): string - { - if (\is_object($value)) { - return \get_class($value); - } - - if (\is_array($value)) { - return 'array'; - } - - if (\is_string($value)) { - return '"' . $value . '"'; - } - - if (\is_resource($value)) { - return 'resource'; - } - - if ($value === null) { - return 'null'; - } - - if ($value === false) { - return 'false'; - } - - if ($value === true) { - return 'true'; - } - - return (string) $value; - } - - /** - * @param mixed[] $values - */ - private function formatValues(array $values): string - { - foreach ($values as $key => $value) { - $values[$key] = $this->formatValue($value); - } - - return \implode(', ', $values); - } - - /** - * @param mixed $value - */ - protected function datasetLabel(string $name, $value): string - { - if (\is_array($value)) { - return \sprintf('%s: [%s]', $name, $this->formatValues($value)); - } - - return \sprintf('%s: %s', $name, $this->formatValue($value)); - } - - /** - * @return mixed[] - */ - public function providerConfiguration(): array - { - $data = []; - - // Valid locales. - foreach (['en', 'EN', 'en-US', 'en_US', 'EN_us', 'EN-US'] as $value) { - $name = 'locale'; - $label = $this->datasetLabel($name, $value); - $config = [ - $name => $value, - 'native' => true, - ]; - $expected = [ - $name => 'en', - 'native' => false, - ]; - - $data[$label] = [$config, $expected]; - } - - // Invalid locales. - foreach (['100-134', 'foo_bar', 'english'] as $value) { - $name = 'locale'; - $label = $this->datasetLabel($name, $value); - $config = [ - $name => $value, - 'native' => true, - ]; - $expected = []; - $exception = $this->createInvalidOptionException($name, $value); - - $data[$label] = [$config, $expected, $exception]; - } - - $excludeShortcodes = [ - [':', []], - ['foo bar', 'foo-bar'], - ['foo:bar', 'foo-bar'], - [':foo-bar:', 'foo-bar'], - ['(FOO bar)', 'foo-bar'], - ['[foo:BAR]', 'foo-bar'], - ['{foo bar}', 'foo-bar'], - ['Foo_Bar', 'foo-bar'], - [['foo bar', 'foo:bar'], 'foo-bar'], - ]; - foreach ($excludeShortcodes as $excludeShortcode) { - [$raw, $expected] = $excludeShortcode; - $name = 'exclude.shortcodes'; - $label = $this->datasetLabel($name, $raw); - - $data[$label] = [ - [$name => $raw], - [$name => (array) $expected], - ]; - } - - // Valid presets. - foreach (ShortcodeInterface::SUPPORTED_PRESETS as $value) { - $name = 'preset'; - $label = $this->datasetLabel($name, $value); - $native = \strpos($value, 'native') !== false; - $config = [ - $name => $value, - ]; - if ($native) { - $config['locale'] = 'ko'; - } - - $expected = [ - $name => \array_values(\array_unique(\array_filter([ - $native ? ShortcodeInterface::PRESET_CLDR_NATIVE : null, - ShortcodeInterface::PRESET_ALIASES[$value] ?? $value, - ]))), - ]; - - $data[$label] = [$config, $expected]; - } - - // Invalid presets. - foreach (['100-134', 'foo_bar', 'english'] as $value) { - $name = 'preset'; - $label = $this->datasetLabel($name, $value); - $config = [$name => $value]; - $expected = []; - $exception = $this->createInvalidOptionException($name, $value, ShortcodeInterface::SUPPORTED_PRESETS); - - $data[$label] = [$config, $expected, $exception]; - } - - return $data; - } - - public function testCreate(): void - { - $configuration = new Configuration(); - $this->assertSame($configuration, Configuration::create($configuration)); - - $data = ['exclude.shortcodes' => ['foo-bar']]; - $configuration = new Configuration(new \ArrayObject($data)); - $actual = $configuration->export(); - - $this->assertArrayHasKey('exclude', $actual); - $this->assertArrayHasKey('shortcodes', $actual['exclude']); - $this->assertEmpty(\array_diff($data['exclude.shortcodes'], $actual['exclude']['shortcodes'])); - } - - /** - * @dataProvider providerConfiguration - * - * @param mixed[] $data - * @param mixed[] $expected - */ - public function testConfiguration(array $data, array $expected, ?InvalidConfigurationException $exception = null): void - { - if ($exception) { - $this->expectExceptionObject($exception); - } - - $configuration = Configuration::create($data); - foreach ($configuration as $name => $value) { - if (! isset($expected[$name])) { - continue; - } - - $this->assertSame($expected[$name], $value); - } - - foreach ($expected as $name => $value) { - $this->assertSame($value, $configuration->get($name)); - } - } -} diff --git a/tests/unit/ConverterTest.php b/tests/unit/ConverterTest.php deleted file mode 100644 index 9a5656d..0000000 --- a/tests/unit/ConverterTest.php +++ /dev/null @@ -1,229 +0,0 @@ - [ - 'raw' => '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on a ✈๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some :kangaroo:! <3 Remember to ๐Ÿ“ฑ :D', - 'html' => '🙍🏿‍♂️ is leaving on a ✈๏ธ. Going to ' . - '🇦🇺. Might see some 🦘! ❤ Remember to 📱 😀', - 'shortcode' => ':man-frowning-tone5: is leaving on a :airplane:๏ธ. Going to :flag-au:. ' . - 'Might see some :kangaroo:! :red-heart: Remember to :mobile-phone: :grinning-face:', - 'unicode' => '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on a โœˆ๏ธ๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some ๐Ÿฆ˜! โค๏ธ Remember to ๐Ÿ“ฑ ๐Ÿ˜€', - ], - ]; - - /** @var Converter */ - protected $converter; - - /** - * @return mixed[] - */ - public function providerEncodings(): array - { - $data = []; - $data['T_EMOTICON'] = [Lexer::T_EMOTICON, self::ENCODINGS['en']['raw']]; - $data['T_HTML_ENTITY'] = [Lexer::T_HTML_ENTITY, self::ENCODINGS['en']['html']]; - $data['T_SHORTCODE'] = [Lexer::T_SHORTCODE, self::ENCODINGS['en']['shortcode']]; - $data['T_UNICODE'] = [Lexer::T_UNICODE, self::ENCODINGS['en']['unicode']]; - - return $data; - } - - /** - * @return mixed[] - */ - public function providerLocalPresets(): array - { - $data = []; - foreach (DatasetInterface::SUPPORTED_LOCALES as $locale) { - foreach (ShortcodeInterface::PRESETS as $preset) { - $label = \sprintf('%s:%s', $locale, $preset); - if (\file_exists(\sprintf('%s/%s/%s.gz', Dataset::DIRECTORY, $locale, $preset))) { - $data[$label] = [$locale, $preset]; - } else { - $data[$label] = [ - $locale, - $preset, - LocalePresetException::class, - ]; - } - } - } - - return $data; - } - - /** - * @return mixed[] - */ - public function providerPresentation(): array - { - $data = []; - $data['-default-'] = [[], ':smiling-face:', 'โ˜บ๏ธ']; - $data['AUTO'] = [['presentation' => DatasetInterface::AUTO], ':smiling-face:', 'โ˜บ๏ธŽ']; - $data['EMOJI'] = [['presentation' => DatasetInterface::EMOJI], ':smiling-face:', 'โ˜บ๏ธ']; - $data['TEXT'] = [['presentation' => DatasetInterface::TEXT], ':smiling-face:', 'โ˜บ๏ธŽ']; - - return $data; - } - - /** - * @return mixed[] - */ - public function providerShortcodePresets(): array - { - $data = []; - foreach (ShortcodeInterface::PRESETS as $preset) { - $file = \sprintf('%s/../fixtures/%s.md', __DIR__, $preset); - if (\file_exists($file) && ($contents = \file_get_contents($file))) { - $data[$preset] = [$preset, $contents]; - } - } - - foreach (ShortcodeInterface::PRESET_ALIASES as $alias => $preset) { - $file = \sprintf('%s/../fixtures/%s.md', __DIR__, $preset); - if (\file_exists($file) && ($contents = \file_get_contents($file))) { - $data[$alias] = [$alias, $contents]; - } - } - - return $data; - } - - public function testConvert(): void - { - $converter = new Converter(); - $actual = $converter->convert(self::ENCODINGS['en']['raw']); - $this->assertEquals(self::ENCODINGS['en']['unicode'], $actual); - } - - /** - * @dataProvider providerEncodings - */ - public function testConvertToEncoding(int $tokenType, string $expected): void - { - $converter = new Converter(); - $actual = null; - switch ($tokenType) { - case Lexer::T_EMOTICON: - $actual = $converter->convertToEmoticon(self::ENCODINGS['en']['raw']); - break; - - case Lexer::T_HTML_ENTITY: - $actual = $converter->convertToHtml(self::ENCODINGS['en']['raw']); - break; - - case Lexer::T_SHORTCODE: - $actual = $converter->convertToShortcode(self::ENCODINGS['en']['raw']); - break; - - case Lexer::T_UNICODE: - $actual = $converter->convertToUnicode(self::ENCODINGS['en']['raw']); - break; - } - - $this->assertEquals($expected, $actual); - } - - /** - * @dataProvider providerLocalPresets - */ - public function testCreate(string $locale, string $preset, ?string $exception = null): void - { - if ($exception) { - $this->expectException($exception); - $this->expectExceptionMessage(\sprintf( - "Attempted to load the locale \"%s\" dataset. However, the following preset(s) were unable to be loaded:\n%s", - $locale, - \sprintf('%s: The following file does not exist or is not readable: %s', $preset, \sprintf( - '%s/%s/%s.gz', - Dataset::DIRECTORY, - $locale, - $preset - )) - )); - } - - $configuration = [ - 'locale' => $locale, - 'native' => false, - 'preset' => $preset, - ]; - $converter = new Converter($configuration); - $this->assertTrue($converter instanceof Converter); - - // The variable must be manually emptied after each assertion in order to avoid memory leaks between tests. - $converter = null; - } - - public function testExcludedShortcodes(): void - { - $converter = Converter::create( - [ - 'exclude.shortcodes' => ['mobile-phone'], - ] - ); - $this->assertEquals(':iphone:', $converter->convert('๐Ÿ“ฑ', Lexer::T_SHORTCODE)); - } - - /** - * @dataProvider providerPresentation - * - * @param mixed[] $configuration - */ - public function testPresentation(array $configuration, string $raw, string $expected): void - { - $converter = Converter::create($configuration); - $actual = $converter->convertToUnicode($raw); - $this->assertSame($expected, $actual); - } - - public function testReadme(): void - { - $converter = new Converter(); - - $unicode = $converter->convert('We <3 :unicorn: :D!'); - $this->assertSame('We โค๏ธ ๐Ÿฆ„ ๐Ÿ˜€!', $unicode); - - $html = $converter->convertToHtml('We <3 :unicorn: :D!'); - $this->assertSame('We ❤ 🦄 😀!', $html); - - $shortcode = $converter->convertToShortcode('We <3 :unicorn: :D!'); - $this->assertSame('We :red-heart: :unicorn-face: :grinning-face:!', $shortcode); - } - - /** - * @dataProvider providerShortcodePresets - */ - public function testShortcodeToUnicodePresets(string $preset, string $contents): void - { - $converter = Converter::create(['preset' => $preset]); - $expected = \file_get_contents(__DIR__ . '/../fixtures/unicode.md'); - $actual = $converter->convert($contents, Lexer::T_UNICODE); - $this->assertEquals($expected, $actual); - } - - /** - * @dataProvider providerShortcodePresets - */ - public function testUnicodeToShortcodePresets(string $preset, string $expected): void - { - $converter = Converter::create(['preset' => $preset]); - $contents = \file_get_contents(__DIR__ . '/../fixtures/unicode.md'); - $actual = $converter->convert($contents, Lexer::T_SHORTCODE); - $this->assertEquals($expected, $actual); - } -} diff --git a/tests/unit/Dataset/DatasetTest.php b/tests/unit/Dataset/DatasetTest.php new file mode 100644 index 0000000..acc5c80 --- /dev/null +++ b/tests/unit/Dataset/DatasetTest.php @@ -0,0 +1,58 @@ +assertSame(EmojiTest::GRINNING_FACE['hexcode'], \current(\array_keys($dataset->getArrayCopy()))); + $this->assertEquals($dataset[EmojiTest::GRINNING_FACE['hexcode']], $emoji); + } + + public function testCreate(): void + { + $dataset = new Dataset(); + $this->assertSame(0, $dataset->count()); + + $emoji = new Emoji(EmojiTest::GRINNING_FACE); + $dataset = new Dataset($emoji); + $this->assertSame(1, $dataset->count()); + + $dataset = new Dataset([$emoji, $emoji]); + $this->assertSame(1, $dataset->count()); + + $this->assertEquals('UnicornFail\Emoji\Dataset\Dataset', \get_class($dataset)); + $this->assertTrue(new Dataset($emoji) instanceof Dataset); + $this->assertTrue(new Dataset($dataset) instanceof Dataset); + + $this->expectExceptionObject( + new \RuntimeException(\sprintf('Passed array item must be an instance of %s.', Emoji::class)) + ); + $this->assertTrue(new Dataset(1) instanceof Dataset); + } + + public function testFilter(): void + { + $grinningFace = new Emoji(EmojiTest::GRINNING_FACE); + $wavingHand = new Emoji(EmojiTest::WAVING_HAND); + $dataset = new Dataset([$grinningFace, $wavingHand]); + $this->assertSame(7, $dataset->count()); + + $emoticons = $dataset->filter( + static function (Emoji $emoji) { + return $emoji->emoticon !== null; + } + )->indexBy('emoticon'); + $this->assertSame(1, $emoticons->count()); + } +} diff --git a/tests/unit/EmojiTest.php b/tests/unit/Dataset/EmojiTest.php similarity index 92% rename from tests/unit/EmojiTest.php rename to tests/unit/Dataset/EmojiTest.php index 4deb40d..6c891d1 100644 --- a/tests/unit/EmojiTest.php +++ b/tests/unit/Dataset/EmojiTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace UnicornFail\Emoji\Tests\Unit; +namespace UnicornFail\Emoji\Tests\Unit\Dataset; use PHPUnit\Framework\TestCase; -use UnicornFail\Emoji\Emoji; +use UnicornFail\Emoji\Dataset\Emoji; class EmojiTest extends TestCase { @@ -165,6 +165,8 @@ protected function assertEmojiData(array $data, array $expectedData): void */ public static function providerEmojis(): array { + $data = []; + $data['grinning face'] = [ self::GRINNING_FACE, [ @@ -359,9 +361,8 @@ public function testArrayAccess(): void // ::offsetGet $this->assertSame([], $emoji['tone']); - // Ensure getMethods are accessible. + // Ensure dynamic properties are accessible. $emoji = new Emoji(self::GRINNING_FACE); - $this->assertSame('&#x' . self::GRINNING_FACE['hexcode'] . ';', $emoji->getHtmlEntity); $this->assertSame('&#x' . self::GRINNING_FACE['hexcode'] . ';', $emoji['htmlEntity']); $this->expectExceptionObject(new \OutOfRangeException('Unknown property: foo')); @@ -378,8 +379,7 @@ public function testArrayAccess(): void */ public function testCreate(array $data, array $expectedData): void { - $emoji = new Emoji($data); - $this->assertTrue($emoji instanceof Emoji); + $this->assertTrue(new Emoji($data) instanceof Emoji); } /** @@ -393,18 +393,45 @@ public function testGet(array $data, array $expectedData): void $this->assertEmojiData($data, $expectedData); } + public function testRenderer(): void + { + $emoji = new Emoji(self::GRINNING_FACE); + + $this->assertSame(self::GRINNING_FACE['emoji'], $emoji->render()); + + $stringable = new class implements \Stringable { + public function __toString(): string + { + return 'foo'; + } + }; + + $emoji->setRenderer(static function (Emoji $emoji) use ($stringable) { + return $stringable; + }); + + $rendered = $emoji->render(); + + $this->assertSame($stringable, $rendered); + $this->assertSame('foo', (string) $rendered); + } + public function testSerialize(): void { - $emoji = new Emoji(self::GRINNING_FACE); - $actual = \hash('sha256', \serialize($emoji)); - $expected = '6390f5490086baed7ac4ffb76cf4f06647da58ad7dce460a1fb8c672a6af8a91'; + $emoji = new Emoji(self::GRINNING_FACE); + $actual = \hash('sha256', \serialize($emoji)); // Apparently serialization spits out something a bit different in PHP 7.4+. if (\version_compare(PHP_VERSION, '7.4.0', '>=')) { - $expected = '56ba0c30f33f29f6c39197ef09fe531f9127efb795de3473e9b1fb840c1f3a1d'; + $expected = '2a93a7f9ab965d28dbe4ed76edeb0561007509451f67425b5bc9afcde2e90ade'; + } else { + $expected = '54f089d423c2116479b0ee3df084ce4628af4686afc58a8d2cae5bbe6d62b161'; } $this->assertSame($expected, $actual); + + $json = \json_encode($emoji, JSON_UNESCAPED_UNICODE); + $this->assertSame('"' . self::GRINNING_FACE['emoji'] . '"', $json); } public function testToString(): void diff --git a/tests/unit/Dataset/RuntimeDatasetTest.php b/tests/unit/Dataset/RuntimeDatasetTest.php new file mode 100644 index 0000000..50c458d --- /dev/null +++ b/tests/unit/Dataset/RuntimeDatasetTest.php @@ -0,0 +1,191 @@ +getName())); + \touch($temporaryFile); + } + + $this->temporaryFiles[] = $temporaryFile; + + return $temporaryFile; + } + + protected function tearDown(): void + { + foreach ($this->temporaryFiles as $temporaryFile) { + @\unlink($temporaryFile); + } + } + + public function testArchive(): void + { + $temp = $this->createTemporaryFile(); + $emoji = new Emoji(EmojiTest::GRINNING_FACE); + $dataset = new Dataset($emoji); + \file_put_contents($temp, RuntimeDataset::archive($dataset)); + $this->assertNotEmpty(\filesize($temp)); + + $archived = RuntimeDataset::unarchive($temp); + $this->assertEquals('UnicornFail\Emoji\Dataset\Dataset', \get_class($archived)); + $this->assertEquals($dataset->getArrayCopy(), $archived->getArrayCopy()); + } + + public function testEmptyArchive(): void + { + $temp = $this->createTemporaryFile(); + $this->expectException(UnarchiveException::class); + $this->expectExceptionMessage(\sprintf('Empty or corrupted archive: %s.', $temp)); + RuntimeDataset::unarchive($temp); + } + + public function testMalformedArchive(): void + { + $temp = $this->createTemporaryFile(); + + \file_put_contents($temp, (string) \gzencode((string) \file_get_contents(__FILE__), 9)); + + $this->expectException(MalformedArchiveException::class); + $this->expectExceptionMessage(\sprintf('Malformed archive %s. Perhaps it is corrupted or was archived using an older API. Try recreating the archive.', $temp)); + RuntimeDataset::unarchive($temp); + } + + public function testMalformedArchive2(): void + { + $temp = $this->createTemporaryFile(); + \file_put_contents($temp, \gzencode(\serialize(['foo', 'bar', 'baz']), 9)); + $this->expectException(MalformedArchiveException::class); + $this->expectExceptionMessage(\sprintf('Malformed archive %s. Perhaps it is corrupted or was archived using an older API. Try recreating the archive.', $temp)); + RuntimeDataset::unarchive($temp); + } + + public function testMissingArchive(): void + { + $this->expectException(FileNotFoundException::class); + $this->expectExceptionMessage('The following file does not exist or is not readable: foo-bar'); + RuntimeDataset::unarchive('foo-bar'); + } + + public function testArrayObject(): void + { + $grinningFace = new Emoji(EmojiTest::GRINNING_FACE); + $wavingHand = new Emoji(EmojiTest::WAVING_HAND); + $dataset = new Dataset([$grinningFace, $wavingHand]); + $runtime = new RuntimeDataset(new Configuration(), $dataset); + + $this->assertTrue($runtime->offsetExists(EmojiTest::GRINNING_FACE['hexcode'])); + + $this->assertSame(7, $runtime->count()); + $this->assertSame($grinningFace, $runtime->current()); + $this->assertSame(EmojiTest::GRINNING_FACE['hexcode'], $runtime->key()); + + $runtime->seek(1); + + $this->assertSame($wavingHand, $runtime->current()); + $this->assertSame(EmojiTest::WAVING_HAND['hexcode'], $runtime->key()); + + $runtime->rewind(); + + $this->assertSame($grinningFace, $runtime->current()); + + $runtime->next(); + $this->assertSame($wavingHand, $runtime->current()); + + $this->assertTrue($runtime->valid()); + } + + public function testFilter(): void + { + $grinningFace = new Emoji(EmojiTest::GRINNING_FACE); + $wavingHand = new Emoji(EmojiTest::WAVING_HAND); + $dataset = new Dataset([$grinningFace, $wavingHand]); + $runtime = new RuntimeDataset(new Configuration(), $dataset); + + $this->assertSame(7, $runtime->count()); + + $filtered = $runtime->filter( + static function (Emoji $emoji) { + return $emoji->emoticon !== null; + } + ); + + $this->assertSame(RuntimeDataset::class, \get_class($filtered)); + + $this->assertSame(1, $filtered->indexBy('emoticon')->count()); + } + + public function testGetPresets(): void + { + $environment = new Environment(['locale' => 'en', 'preset' => 'github']); + $runtime = $environment->getRuntimeDataset(); + + $this->assertEquals([ + EmojibaseShortcodeInterface::PRESET_GITHUB, + ], $runtime->getPresets()); + + $environment = new Environment(['locale' => 'ja']); + $runtime = $environment->getRuntimeDataset(); + + $this->assertEquals([ + EmojibaseShortcodeInterface::PRESET_CLDR_NATIVE, + EmojibaseShortcodeInterface::PRESET_EMOJIBASE, + EmojibaseShortcodeInterface::PRESET_CLDR, + ], $runtime->getPresets()); + + $environment = new Environment(['locale' => 'ja', 'native' => false]); + $runtime = $environment->getRuntimeDataset(); + + $this->assertEquals([ + EmojibaseShortcodeInterface::PRESET_EMOJIBASE, + EmojibaseShortcodeInterface::PRESET_CLDR, + ], $runtime->getPresets()); + } + + /** + * @return mixed[] + */ + public function providerImmutabilityMethods(): array + { + $methods = [['offsetSet'], ['offsetUnset']]; + + return (array) \array_combine(\array_map('current', $methods), $methods); + } + + /** + * @dataProvider providerImmutabilityMethods + */ + public function testImmutability(string $method): void + { + $grinningFace = new Emoji(EmojiTest::GRINNING_FACE); + $wavingHand = new Emoji(EmojiTest::WAVING_HAND); + $dataset = new Dataset([$grinningFace, $wavingHand]); + $runtime = new RuntimeDataset(new Configuration(), $dataset); + + $this->expectExceptionObject(new \BadMethodCallException('Unable to modify immutable object.')); + $runtime->$method('foo', 'bar'); + } +} diff --git a/tests/unit/DatasetTest.php b/tests/unit/DatasetTest.php deleted file mode 100644 index 458f1b1..0000000 --- a/tests/unit/DatasetTest.php +++ /dev/null @@ -1,125 +0,0 @@ -temporaryFiles[] = $temporaryFile; - - return $temporaryFile; - } - - protected function tearDown(): void - { - foreach ($this->temporaryFiles as $temporaryFile) { - @\unlink($temporaryFile); - } - } - - public function testArchive(): void - { - $temp = $this->createTemporaryFile(); - $emoji = new Emoji(EmojiTest::GRINNING_FACE); - $dataset = new Dataset($emoji); - \file_put_contents($temp, $dataset->archive()); - $this->assertNotEmpty(\filesize($temp)); - - $archived = Dataset::unarchive($temp); - $this->assertTrue($archived instanceof Dataset); - $this->assertEquals($dataset->getArrayCopy(), $archived->getArrayCopy()); - } - - public function testEmptyArchive(): void - { - $temp = $this->createTemporaryFile(); - $this->expectException(UnarchiveException::class); - $this->expectExceptionMessage(\sprintf('Empty or corrupted archive: %s.', $temp)); - Dataset::unarchive($temp); - } - - public function testMalformedArchive(): void - { - $temp = $this->createTemporaryFile(); - \file_put_contents($temp, \gzencode(\file_get_contents(__FILE__), 9)); - $this->expectException(MalformedArchiveException::class); - $this->expectExceptionMessage(\sprintf('Malformed archive %s. Perhaps it is corrupted or was archived using an older API. Try recreating the archive.', $temp)); - Dataset::unarchive($temp); - } - - public function testMalformedArchive2(): void - { - $temp = $this->createTemporaryFile(); - \file_put_contents($temp, \gzencode(\serialize(['foo', 'bar', 'baz']), 9)); - $this->expectException(MalformedArchiveException::class); - $this->expectExceptionMessage(\sprintf('Malformed archive %s. Perhaps it is corrupted or was archived using an older API. Try recreating the archive.', $temp)); - Dataset::unarchive($temp); - } - - public function testMissingArchive(): void - { - $this->expectException(FileNotFoundException::class); - $this->expectExceptionMessage('The following file does not exist or is not readable: foo-bar'); - Dataset::unarchive('foo-bar'); - } - - public function testArrayAccess(): void - { - $emoji = new Emoji(EmojiTest::GRINNING_FACE); - $dataset = new Dataset($emoji); - - $this->assertSame(EmojiTest::GRINNING_FACE['hexcode'], \current(\array_keys($dataset->getArrayCopy()))); - $this->assertEquals($dataset[EmojiTest::GRINNING_FACE['hexcode']], $emoji); - } - - public function testCreate(): void - { - $dataset = new Dataset(); - $this->assertSame(0, $dataset->count()); - - $emoji = new Emoji(EmojiTest::GRINNING_FACE); - $dataset = new Dataset($emoji); - $this->assertSame(1, $dataset->count()); - - $dataset = new Dataset([$emoji, $emoji]); - $this->assertSame(1, $dataset->count()); - - $this->assertTrue($dataset instanceof Dataset); - $this->assertTrue(new Dataset($emoji) instanceof Dataset); - $this->assertTrue(new Dataset($dataset) instanceof Dataset); - - $this->expectExceptionObject( - new \RuntimeException(\sprintf('Passed array item must be an instance of %s.', Emoji::class)) - ); - $this->assertTrue(new Dataset(1) instanceof Dataset); - } - - public function testFilter(): void - { - $grinningFace = new Emoji(EmojiTest::GRINNING_FACE); - $wavingHand = new Emoji(EmojiTest::WAVING_HAND); - $dataset = new Dataset([$grinningFace, $wavingHand]); - $this->assertSame(7, $dataset->count()); - - $emoticons = $dataset->filter( - static function (Emoji $emoji) { - return $emoji->emoticon !== null; - } - )->indexBy('emoticon'); - $this->assertSame(1, $emoticons->count()); - } -} diff --git a/tests/unit/EmojiConverterTest.php b/tests/unit/EmojiConverterTest.php new file mode 100644 index 0000000..2e04a71 --- /dev/null +++ b/tests/unit/EmojiConverterTest.php @@ -0,0 +1,308 @@ + [ + 'raw' => '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on an ✈๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some :kangaroo:! <3 Remember to ๐Ÿ“ฑ :D', + 'html' => '🙍🏿‍♂️ is leaving on an ✈๏ธ. Going to ' . + '🇦🇺. Might see some 🦘! ❤ Remember to 📱 😀', + 'shortcode' => ':man-frowning-tone5: is leaving on an :airplane:๏ธ. Going to :australia:. ' . + 'Might see some :kangaroo:! :heart: Remember to :android: :grinning:', + 'unicode' => '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on an โœˆ๏ธ๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some ๐Ÿฆ˜! โค๏ธ Remember to ๐Ÿ“ฑ ๐Ÿ˜€', + ], + ]; + + /** @var EmojiConverter */ + protected $converter; + + /** + * @return mixed[] + */ + public function providerEncodings(): array + { + $data = []; + $data['T_EMOTICON'] = [EmojiLexer::T_EMOTICON, self::ENCODINGS['en']['raw']]; + $data['T_HTML_ENTITY'] = [EmojiLexer::T_HTML_ENTITY, self::ENCODINGS['en']['html']]; + $data['T_SHORTCODE'] = [EmojiLexer::T_SHORTCODE, self::ENCODINGS['en']['shortcode']]; + $data['T_UNICODE'] = [EmojiLexer::T_UNICODE, self::ENCODINGS['en']['unicode']]; + + return $data; + } + + /** + * @return mixed[] + */ + public function providerLocalPresets(): array + { + $locales = \array_merge(['en-US'], EmojibaseDatasetInterface::SUPPORTED_LOCALES); + + $data = []; + foreach ($locales as $locale) { + foreach (EmojibaseShortcodeInterface::PRESETS as $preset) { + $originalLocale = $locale; + $label = \sprintf('%s:%s', $locale, $preset); + + if ($locale === 'en-US') { + $locale = 'en'; + } + + $native = false; + if ($preset === EmojibaseShortcodeInterface::PRESET_CLDR_NATIVE) { + $native = null; + } + + $exception = null; + if (! \file_exists(\sprintf('%s/%s/%s.gz', Dataset::DIRECTORY, $locale, $preset))) { + $exception = LocalePresetException::class; + } + + $data[$label] = [$locale, $preset, $exception, $originalLocale, $native]; + } + } + + return $data; + } + + /** + * @return mixed[] + */ + public function providerPresentation(): array + { + $data = []; + $data['-default-'] = [[], ':smiling-face:', 'โ˜บ๏ธ']; + $data['AUTO'] = [['presentation' => EmojibaseDatasetInterface::AUTO], ':smiling-face:', 'โ˜บ๏ธŽ']; + $data['EMOJI'] = [['presentation' => EmojibaseDatasetInterface::EMOJI], ':smiling-face:', 'โ˜บ๏ธ']; + $data['TEXT'] = [['presentation' => EmojibaseDatasetInterface::TEXT], ':smiling-face:', 'โ˜บ๏ธŽ']; + + return $data; + } + + /** + * @return mixed[] + */ + public function providerShortcodePresets(): array + { + $data = []; + foreach (EmojibaseShortcodeInterface::PRESETS as $preset) { + $file = \sprintf('%s/../fixtures/%s.md', __DIR__, $preset); + if (\file_exists($file) && ($contents = \file_get_contents($file))) { + $data[$preset] = [$preset, $contents]; + } + } + + foreach (EmojibaseShortcodeInterface::PRESET_ALIASES as $alias => $preset) { + $file = \sprintf('%s/../fixtures/%s.md', __DIR__, $preset); + if (\file_exists($file) && ($contents = \file_get_contents($file))) { + $data[$alias] = [$alias, $contents]; + } + } + + return $data; + } + + /** + * @dataProvider providerEncodings + */ + public function testConvert(int $tokenType, string $expected): void + { + $actual = $this->convertTo(EmojiConverterInterface::TYPES[$tokenType], self::ENCODINGS['en']['raw']); + $this->assertEquals($expected, $actual); + } + + public function testInvoke(): void + { + /** @var EmojiConverter $converter */ + $converter = EmojiConverter::create(); + $this->assertEquals(self::ENCODINGS['en']['unicode'], $converter(self::ENCODINGS['en']['raw'])); + } + + /** + * @dataProvider providerLocalPresets + * + * @psalm-param ?class-string<\Throwable> $exception + */ + public function testLocalePresets(string $locale, string $preset, ?string $exception = null, ?string $originalLocal = null, ?bool $native = null): void + { + if ($exception !== null) { + $this->expectException($exception); + $this->expectExceptionMessage(\sprintf( + "Attempted to load the locale \"%s\" dataset. However, the following preset(s) were unable to be loaded:\n%s", + $originalLocal ?? $locale, + \sprintf('%s: The following file does not exist or is not readable: %s', $preset, \sprintf( + '%s/%s/%s.gz', + Dataset::DIRECTORY, + $locale, + $preset + )) + )); + } + + $configuration = [ + 'locale' => $originalLocal ?? $locale, + 'native' => $native, + 'preset' => $preset, + ]; + $environment = Environment::create($configuration); + $converter = new EmojiConverter($environment); + + $this->assertSame($environment, $converter->getEnvironment()); + $this->assertTrue($environment->getRuntimeDataset() instanceof RuntimeDataset); + + // Convert so the parser actually runs the various parser/lexer code related to the local preset. + $converter->convert('Test'); + + // The variable must be manually emptied after each assertion in order to avoid memory leaks between tests. + $converter = null; + } + + public function testUnknownPreset(): void + { + $environment = Environment::create([ + 'preset' => [EmojibaseShortcodeInterface::PRESET_CLDR_NATIVE, 'foo'], + ]); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage("The item 'presetย โ€บย 1' expects to be 'cldr'|'cldr-native'|'emojibase'|'emojibase...'|'github'|'iamcal'|'joypixels'|'discord'|'slack', 'foo' given."); + + // Trigger initialize so the configuration is built and validated. + $environment->getRuntimeDataset(); + } + + public function testCustomParserRenderer(): void + { + $input = 'foo'; + $document = $this->createMock(Document::class); + + $parser = new class ($document) implements EmojiParserInterface { + /** @var Document */ + private $document; + + public function __construct(Document $document) + { + $this->document = $document; + } + + public function parse(string $input): Document + { + return $this->document; + } + }; + + $renderer = new class ($input) implements DocumentRendererInterface { + /** @var string */ + private $content; + + public function __construct(string $content) + { + $this->content = $content; + } + + public function renderDocument(Document $document): string + { + return $this->content; + } + }; + + $environment = new Environment(); + $converter = new EmojiConverter($environment, $parser, $renderer); + + $this->assertSame($environment, $converter->getEnvironment()); + $this->assertSame($parser, $converter->getParser()); + $this->assertSame($renderer, $converter->getRenderer()); + + $this->assertSame($document, $parser->parse($input)); + $this->assertSame($input, $renderer->renderDocument($document)); + $this->assertSame($input, (string) $converter->convert($input)); + } + + /** @param array $config */ + protected function convert(string $input, array $config = []): string + { + $converter = EmojiConverter::create($config); + + return (string) $converter->convert($input); + } + + /** + * @param string|string[] $type + * @param array $config + */ + protected function convertTo($type, string $input, array $config = []): string + { + $config['convert'] = $type; + + return $this->convert($input, $config); + } + + public function testExcludedShortcodes(): void + { + $this->assertEquals(':iphone:', $this->convertTo(EmojiConverter::SHORTCODE, '๐Ÿ“ฑ', [ + 'exclude' => [ + 'shortcodes' => ['mobile-phone', 'android'], + ], + ])); + } + + /** + * @dataProvider providerPresentation + * + * @param array $configuration + */ + public function testPresentation(array $configuration, string $raw, string $expected): void + { + $actual = $this->convert($raw, $configuration); + $this->assertSame($expected, $actual); + } + + public function testReadme(): void + { + $unicode = $this->convert('We <3 :unicorn: :D!'); + $this->assertSame('We โค๏ธ ๐Ÿฆ„ ๐Ÿ˜€!', $unicode); + + $html = $this->convertTo(EmojiConverter::HTML_ENTITY, 'We <3 :unicorn: :D!'); + $this->assertSame('We ❤ 🦄 😀!', $html); + + $shortcode = $this->convertTo(EmojiConverter::SHORTCODE, 'We <3 :unicorn: :D!'); + $this->assertSame('We :heart: :unicorn: :grinning:!', $shortcode); + } + + /** + * @dataProvider providerShortcodePresets + */ + public function testShortcodeToUnicodePresets(string $preset, string $contents): void + { + $expected = (string) \file_get_contents(__DIR__ . '/../fixtures/unicode.md'); + $actual = $this->convert($contents, ['preset' => $preset]); + $this->assertEquals($expected, $actual); + } + + /** + * @dataProvider providerShortcodePresets + */ + public function testUnicodeToShortcodePresets(string $preset, string $expected): void + { + $contents = (string) \file_get_contents(__DIR__ . '/../fixtures/unicode.md'); + $actual = $this->convertTo(EmojiConverter::SHORTCODE, $contents, ['preset' => $preset]); + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/unit/Environment/EnvironmentTest.php b/tests/unit/Environment/EnvironmentTest.php new file mode 100644 index 0000000..2e676f1 --- /dev/null +++ b/tests/unit/Environment/EnvironmentTest.php @@ -0,0 +1,287 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Environment; + +use League\Configuration\ConfigurationInterface; +use League\Configuration\MutableConfigurationInterface; +use PHPUnit\Framework\TestCase; +use Psr\EventDispatcher\EventDispatcherInterface; +use UnicornFail\Emoji\Environment\Environment; +use UnicornFail\Emoji\Event\AbstractEvent; +use UnicornFail\Emoji\Extension\ExtensionInterface; +use UnicornFail\Emoji\Renderer\NodeRendererInterface; +use UnicornFail\Emoji\Tests\Unit\Event\FakeEvent; +use UnicornFail\Emoji\Tests\Unit\Event\FakeEventListener; +use UnicornFail\Emoji\Tests\Unit\Event\FakeEventListenerInvokable; +use UnicornFail\Emoji\Tests\Unit\Event\FakeEventParent; +use UnicornFail\Emoji\Tests\Unit\Node\Node1; +use UnicornFail\Emoji\Tests\Unit\Node\Node3; +use UnicornFail\Emoji\Util\ArrayCollection; + +class EnvironmentTest extends TestCase +{ + public function testAddGetExtensions(): void + { + $environment = new Environment(); + $this->assertCount(0, $environment->getExtensions()); + + $firstExtension = $this->createMock(ExtensionInterface::class); + $firstExtension->expects($this->once()) + ->method('register') + ->with($environment); + + $environment->addExtension($firstExtension); + + $extensions = $environment->getExtensions(); + $this->assertCount(1, $extensions); + $this->assertEquals($firstExtension, $extensions[0]); + + $secondExtension = $this->createMock(ExtensionInterface::class); + $secondExtension->expects($this->once()) + ->method('register') + ->with($environment); + $environment->addExtension($secondExtension); + + $extensions = $environment->getExtensions(); + + $this->assertCount(2, $extensions); + $this->assertEquals($firstExtension, $extensions[0]); + $this->assertEquals($secondExtension, $extensions[1]); + + // Trigger initialization + $environment->getRuntimeDataset(); + } + + public function testConstructor(): void + { + $environment = new Environment(['allow_unsafe_links' => false]); + $this->assertFalse($environment->getConfiguration()->get('allow_unsafe_links')); + } + + public function testGetConfiguration(): void + { + $environment = new Environment(['allow_unsafe_links' => false]); + + $configuration = $environment->getConfiguration(); + $this->assertInstanceOf(ConfigurationInterface::class, $configuration); + $this->assertNotInstanceOf(MutableConfigurationInterface::class, $configuration); + $this->assertFalse($configuration->get('allow_unsafe_links')); + } + + public function testAddRenderer(): void + { + $environment = new Environment(); + + $renderer = $this->createMock(NodeRendererInterface::class); + $environment->addRenderer('MyClass', $renderer); + + $this->assertContains($renderer, $environment->getRenderersForClass('MyClass')); + } + + public function testAddRendererFailsAfterInitialization(): void + { + $this->expectException(\RuntimeException::class); + + $environment = new Environment(); + + // This triggers the initialization + $environment->getRenderersForClass('MyClass'); + + $renderer = $this->createMock(NodeRendererInterface::class); + $environment->addRenderer('MyClass', $renderer); + } + + public function testGetRendererForUnknownClass(): void + { + $environment = new Environment(); + $mockRenderer = $this->createMock(NodeRendererInterface::class); + $environment->addRenderer(Node3::class, $mockRenderer); + + $this->assertEmpty($environment->getRenderersForClass(Node1::class)); + } + + public function testGetRendererForSubClass(): void + { + $environment = new Environment(); + $mockRenderer = $this->createMock(NodeRendererInterface::class); + $environment->addRenderer(Node1::class, $mockRenderer); + + // Ensure the parent renderer is returned + $this->assertFirstResult($mockRenderer, $environment->getRenderersForClass(Node3::class)); + // Check again to ensure any cached result is also the same + $this->assertFirstResult($mockRenderer, $environment->getRenderersForClass(Node3::class)); + } + + public function testAddExtensionAndGetter(): void + { + $environment = new Environment(); + + $extension = $this->createMock(ExtensionInterface::class); + $environment->addExtension($extension); + + $this->assertContains($extension, $environment->getExtensions()); + } + + public function testAddExtensionFailsAfterInitialization(): void + { + $this->expectException(\RuntimeException::class); + + $environment = new Environment(); + + // This triggers the initialization + $environment->getRenderersForClass('MyClass'); + + $extension = $this->createMock(ExtensionInterface::class); + $environment->addExtension($extension); + } + + public function testInjectableEventListenersGetInjected(): void + { + $environment = new Environment(); + + // phpcs:ignore Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore + $listener1 = new FakeEventListener(static function (): void { }); + // phpcs:ignore Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore + $listener2 = new FakeEventListenerInvokable(static function (): void { }); + + $environment->addEventListener('', [$listener1, 'doStuff']); + $environment->addEventListener('', $listener2); + + // Trigger initialization + $environment->getRuntimeDataset(); + + $this->assertSame($environment, $listener1->getEnvironment()); + $this->assertSame($environment, $listener2->getEnvironment()); + + $this->assertNotNull($listener1->getConfiguration()); + $this->assertNotNull($listener2->getConfiguration()); + } + + public function testRendererPrioritization(): void + { + $environment = new Environment(); + + $renderer1 = $this->createMock(NodeRendererInterface::class); + $renderer2 = $this->createMock(NodeRendererInterface::class); + $renderer3 = $this->createMock(NodeRendererInterface::class); + + $environment->addRenderer('foo', $renderer1); + $environment->addRenderer('foo', $renderer2, 50); + $environment->addRenderer('foo', $renderer3); + + $parsers = \iterator_to_array($environment->getRenderersForClass('foo')); + + $this->assertSame($renderer2, $parsers[0]); + $this->assertSame($renderer1, $parsers[1]); + $this->assertSame($renderer3, $parsers[2]); + } + + public function testEventDispatching(): void + { + $environment = new Environment(); + $event = new FakeEvent(); + + $actualOrder = []; + + $environment->addEventListener(FakeEvent::class, function (FakeEvent $e) use ($event, &$actualOrder): void { + $this->assertSame($event, $e); + $actualOrder[] = 'a'; + }); + + // Listeners on parent classes should also be called + $environment->addEventListener(FakeEventParent::class, function (FakeEvent $e) use ($event, &$actualOrder): void { + $this->assertSame($event, $e); + $actualOrder[] = 'b'; + $e->stopPropagation(); + }); + + $environment->addEventListener(FakeEvent::class, function (FakeEvent $e) use ($event, &$actualOrder): void { + $this->assertSame($event, $e); + $actualOrder[] = 'c'; + }, 10); + + $environment->addEventListener(FakeEvent::class, function (FakeEvent $e): void { + $this->fail('Propogation should have been stopped before here'); + }); + + $environment->dispatch($event); + + $this->assertCount(3, $actualOrder); + $this->assertEquals('c', $actualOrder[0]); + $this->assertEquals('a', $actualOrder[1]); + $this->assertEquals('b', $actualOrder[2]); + } + + public function testAddEventListenerFailsAfterInitialization(): void + { + $this->expectException(\RuntimeException::class); + + $environment = new Environment(); + + // Trigger initialization + $environment->dispatch($this->createMock(AbstractEvent::class)); + + $environment->addEventListener(AbstractEvent::class, static function (AbstractEvent $e): void { + }); + } + + public function testDispatchDelegatesToProvidedDispatcher(): void + { + $dispatchersCalled = new ArrayCollection(); + + $environment = new Environment(); + + $environment->addEventListener(FakeEvent::class, static function (FakeEvent $event) use ($dispatchersCalled): void { + $dispatchersCalled[] = 'THIS SHOULD NOT BE CALLED!'; + }); + + $environment->setEventDispatcher(new class ($dispatchersCalled) implements EventDispatcherInterface { + /** @var ArrayCollection */ + private $dispatchersCalled; + + public function __construct(ArrayCollection $dispatchersCalled) + { + $this->dispatchersCalled = $dispatchersCalled; + } + + public function dispatch(object $event): void + { + $this->dispatchersCalled[] = 'external'; + } + }); + + $environment->dispatch(new FakeEvent()); + + $this->assertCount(1, $dispatchersCalled); + $this->assertSame('external', $dispatchersCalled->first()); + } + + /** + * @param mixed $expected + * @param iterable $actual + */ + private function assertFirstResult($expected, iterable $actual): void + { + foreach ($actual as $a) { + $this->assertSame($expected, $a); + + return; + } + + $this->assertSame($expected, null); + } +} diff --git a/tests/unit/Event/AbstractEventTest.php b/tests/unit/Event/AbstractEventTest.php new file mode 100644 index 0000000..018b434 --- /dev/null +++ b/tests/unit/Event/AbstractEventTest.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Event; + +use PHPUnit\Framework\TestCase; + +final class AbstractEventTest extends TestCase +{ + public function testStopPropagation(): void + { + $event = new FakeEvent(); + + $this->assertFalse($event->isPropagationStopped()); + + $event->stopPropagation(); + $this->assertTrue($event->isPropagationStopped()); + } +} diff --git a/tests/unit/Event/DocumentParsedEventTest.php b/tests/unit/Event/DocumentParsedEventTest.php new file mode 100644 index 0000000..596b317 --- /dev/null +++ b/tests/unit/Event/DocumentParsedEventTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Event; + +use PHPUnit\Framework\TestCase; +use UnicornFail\Emoji\Environment\Environment; +use UnicornFail\Emoji\Event\DocumentParsedEvent; +use UnicornFail\Emoji\Node\Document; +use UnicornFail\Emoji\Parser\EmojiParser; + +final class DocumentParsedEventTest extends TestCase +{ + public function testGetDocument(): void + { + $document = new Document(); + + $event = new DocumentParsedEvent($document); + + $this->assertSame($document, $event->getDocument()); + } + + public function testEventDispatchedAtCorrectTime(): void + { + $wasCalled = false; + + $environment = Environment::create(); + $environment->addEventListener(DocumentParsedEvent::class, static function (DocumentParsedEvent $event) use (&$wasCalled): void { + $wasCalled = true; + }); + + $parser = new EmojiParser($environment); + $parser->parse('hello world'); + + $this->assertTrue($wasCalled); + } +} diff --git a/tests/unit/Event/DocumentPreParsedEventTest.php b/tests/unit/Event/DocumentPreParsedEventTest.php new file mode 100644 index 0000000..181770c --- /dev/null +++ b/tests/unit/Event/DocumentPreParsedEventTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Event; + +use PHPUnit\Framework\TestCase; +use UnicornFail\Emoji\Environment\Environment; +use UnicornFail\Emoji\Event\DocumentPreParsedEvent; +use UnicornFail\Emoji\Node\Document; +use UnicornFail\Emoji\Parser\EmojiParser; + +final class DocumentPreParsedEventTest extends TestCase +{ + public function testGetDocument(): void + { + $document = new Document(); + $input = ''; + + $event = new DocumentPreParsedEvent($document, $input); + + $this->assertSame($document, $event->getDocument()); + $this->assertSame($input, $event->getInput()); + } + + public function testReplaceInput(): void + { + $input = 'foo'; + + $event = new DocumentPreParsedEvent(new Document(), $input); + + $this->assertSame($input, $event->getInput()); + + $newInput = 'bar'; + $event->replaceInput($newInput); + + $this->assertSame($newInput, $event->getInput()); + $this->assertNotSame($input, $event->getInput()); + } + + public function testEventDispatchedAtCorrectTime(): void + { + $wasCalled = false; + + $environment = Environment::create(); + $environment->addEventListener(DocumentPreParsedEvent::class, static function (DocumentPreParsedEvent $event) use (&$wasCalled): void { + $wasCalled = true; + }); + + $parser = new EmojiParser($environment); + $parser->parse('hello world'); + + $this->assertTrue($wasCalled); + } +} diff --git a/tests/unit/Event/DocumentRenderedEventTest.php b/tests/unit/Event/DocumentRenderedEventTest.php new file mode 100644 index 0000000..5c610a0 --- /dev/null +++ b/tests/unit/Event/DocumentRenderedEventTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Event; + +use PHPUnit\Framework\TestCase; +use UnicornFail\Emoji\Environment\Environment; +use UnicornFail\Emoji\Event\DocumentRenderedEvent; +use UnicornFail\Emoji\Node\Document; +use UnicornFail\Emoji\Renderer\DocumentRenderer; + +final class DocumentRenderedEventTest extends TestCase +{ + public function testGettersAndReplacers(): void + { + $content = 'foo'; + + $event = new DocumentRenderedEvent($content); + + $this->assertSame($content, $event->getContent()); + + // Replace the output with something else - the getter should return something different now + $event->replaceContent('bar'); + + $this->assertNotSame($content, $event->getContent()); + } + + public function testEventDispatchedAtCorrectTime(): void + { + $wasCalled = false; + + $environment = Environment::create(); + $environment->addEventListener(DocumentRenderedEvent::class, static function (DocumentRenderedEvent $event) use (&$wasCalled): void { + $wasCalled = true; + $event->replaceContent('foo'); + }); + + $renderer = new DocumentRenderer($environment); + $result = $renderer->renderDocument(new Document()); + + $this->assertTrue($wasCalled); + $this->assertSame('foo', (string) $result); + } +} diff --git a/tests/unit/Event/FakeEvent.php b/tests/unit/Event/FakeEvent.php new file mode 100644 index 0000000..388ee30 --- /dev/null +++ b/tests/unit/Event/FakeEvent.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Event; + +class FakeEvent extends FakeEventParent +{ +} diff --git a/tests/unit/Event/FakeEventListener.php b/tests/unit/Event/FakeEventListener.php new file mode 100644 index 0000000..e764186 --- /dev/null +++ b/tests/unit/Event/FakeEventListener.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Event; + +use League\Configuration\ConfigurationAwareInterface; +use League\Configuration\ConfigurationInterface; +use UnicornFail\Emoji\Environment\EnvironmentAwareInterface; +use UnicornFail\Emoji\Environment\EnvironmentInterface; +use UnicornFail\Emoji\Event\AbstractEvent; + +class FakeEventListener implements ConfigurationAwareInterface, EnvironmentAwareInterface +{ + /** @var callable */ + private $callback; + + /** @var ConfigurationInterface */ + private $configuration; + + /** @var EnvironmentInterface */ + private $environment; + + public function __construct(callable $callback) + { + $this->callback = $callback; + } + + public function setConfiguration(ConfigurationInterface $configuration): void + { + $this->configuration = $configuration; + } + + public function setEnvironment(EnvironmentInterface $environment): void + { + $this->environment = $environment; + } + + public function getConfiguration(): ConfigurationInterface + { + return $this->configuration; + } + + public function getEnvironment(): EnvironmentInterface + { + return $this->environment; + } + + /** + * @return mixed + */ + public function doStuff(AbstractEvent $event) + { + return \call_user_func($this->callback, $event); + } +} diff --git a/tests/unit/Event/FakeEventListenerInvokable.php b/tests/unit/Event/FakeEventListenerInvokable.php new file mode 100644 index 0000000..edf64d2 --- /dev/null +++ b/tests/unit/Event/FakeEventListenerInvokable.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Event; + +use UnicornFail\Emoji\Event\AbstractEvent; + +final class FakeEventListenerInvokable extends FakeEventListener +{ + public function __invoke(AbstractEvent $event): void + { + $this->doStuff($event); + } +} diff --git a/tests/unit/Event/FakeEventParent.php b/tests/unit/Event/FakeEventParent.php new file mode 100644 index 0000000..d7f1a15 --- /dev/null +++ b/tests/unit/Event/FakeEventParent.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Event; + +use UnicornFail\Emoji\Event\AbstractEvent; + +class FakeEventParent extends AbstractEvent +{ +} diff --git a/tests/unit/Extension/TwemojiExtensionTest.php b/tests/unit/Extension/TwemojiExtensionTest.php new file mode 100644 index 0000000..6542517 --- /dev/null +++ b/tests/unit/Extension/TwemojiExtensionTest.php @@ -0,0 +1,123 @@ + $config */ + protected function convert(string $input, array $config = [], bool $setAsDefaultConversionType = true): string + { + $converter = TwemojiConverter::create($config, $setAsDefaultConversionType); + + return (string) $converter->convert($input); + } + + /** + * @param string|string[] $conversionTypes + * @param array $config + */ + protected function convertTo($conversionTypes, string $input, array $config = [], bool $setAsDefaultConversionType = true): string + { + $config['convert'] = $conversionTypes; + + return $this->convert($input, $config, $setAsDefaultConversionType); + } + + public function testConvert(): void + { + $raw = '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on an ✈๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some :kangaroo:! <3 Remember to ๐Ÿ“ฑ :D'; + $expected = 'man frowning: dark skin tone is leaving on an airplane๏ธ. Going to flag: Australia. Might see some kangaroo! red heart Remember to mobile phone grinning face'; + + $actual = $this->convert($raw); + $this->assertEquals($expected, $actual); + } + + public function testPartialConvert(): void + { + $raw = '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on an ✈๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some :kangaroo:! <3 Remember to ๐Ÿ“ฑ :D'; + $expected = 'man frowning: dark skin tone is leaving on an airplane๏ธ. Going to flag: Australia. Might see some :kangaroo:! red heart Remember to mobile phone grinning face'; + + // Keep shortcodes (perhaps parsed using something else). + $conversionTypes = [ + TwemojiConverter::SHORTCODE => TwemojiConverter::SHORTCODE, + ]; + + $actual = $this->convertTo($conversionTypes, $raw); + $this->assertEquals($expected, $actual); + } + + public function testSetAsDefaultConversionType(): void + { + $raw = '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on an ✈๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some :kangaroo:! <3 Remember to ๐Ÿ“ฑ :D'; + $expected = EmojiConverterTest::ENCODINGS['en']['unicode']; + + // Ensure twemoji isn't set as the default conversion type. + $actual = $this->convert($raw, [], false); + $this->assertEquals($expected, $actual); + + $raw = '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on an ✈๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some :kangaroo:! <3 Remember to ๐Ÿ“ฑ :D'; + $expected = '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on an airplane๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some ๐Ÿฆ˜! โค๏ธ Remember to ๐Ÿ“ฑ ๐Ÿ˜€'; + + // Ensure twemoji isn't set as the default conversion type. + $actual = $this->convert($raw, [ + 'convert' => [ + TwemojiConverter::HTML_ENTITY => TwemojiConverter::CONVERSION_TYPE, + ], + ], false); + $this->assertEquals($expected, $actual); + } + + public function testPngType(): void + { + $raw = '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on an ✈๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some :kangaroo:! <3 Remember to ๐Ÿ“ฑ :D'; + $expected = 'man frowning: dark skin tone is leaving on an airplane๏ธ. Going to flag: Australia. Might see some kangaroo! red heart Remember to mobile phone grinning face'; + + $actual = $this->convert($raw, [ + 'twemoji' => [ + 'type' => 'png', + ], + ]); + $this->assertEquals($expected, $actual); + } + + public function testInlineSize(): void + { + $raw = '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on an ✈๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some :kangaroo:! <3 Remember to ๐Ÿ“ฑ :D'; + + $expected = 'man frowning: dark skin tone is leaving on an airplane๏ธ. Going to flag: Australia. Might see some kangaroo! red heart Remember to mobile phone grinning face'; + $actual = $this->convert($raw, [ + 'twemoji' => [ + 'size' => '2.5rem', + ], + ]); + $this->assertEquals($expected, $actual); + + $expected = 'man frowning: dark skin tone is leaving on an airplane๏ธ. Going to flag: Australia. Might see some kangaroo! red heart Remember to mobile phone grinning face'; + $actual = $this->convert($raw, [ + 'twemoji' => [ + 'size' => 2.5, + ], + ]); + $this->assertEquals($expected, $actual); + } + + public function testNonInlineSize(): void + { + $raw = '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ is leaving on an ✈๏ธ. Going to ๐Ÿ‡ฆ๐Ÿ‡บ. Might see some :kangaroo:! <3 Remember to ๐Ÿ“ฑ :D'; + $expected = 'man frowning: dark skin tone is leaving on an airplane๏ธ. Going to flag: Australia. Might see some kangaroo! red heart Remember to mobile phone grinning face'; + + $actual = $this->convert($raw, [ + 'twemoji' => [ + 'inline' => false, + 'size' => 72, + ], + ]); + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/unit/Node/DocumentTest.php b/tests/unit/Node/DocumentTest.php new file mode 100644 index 0000000..e51bba3 --- /dev/null +++ b/tests/unit/Node/DocumentTest.php @@ -0,0 +1,41 @@ +appendNode($node2); + $document->prependNode($node1); + + $this->assertSame([$node1, $node2], $document->getNodes()); + + // Ensure nodes can be replaced. + $node3 = new Node3(); + $document->replaceNode($node2, $node3); + $this->assertSame([$node1, $node3], $document->getNodes()); + + // Ensure trying to replace a non-existent node does nothing. + $mockNode = $this->createMock(Node::class); + $document->replaceNode($mockNode, $node2); + $this->assertSame([$node1, $node3], $document->getNodes()); + + // Ensure nodes can be manipulated via reference. + $nodes = &$document->getNodes(); + $nodes = [$node3, $node2, $mockNode]; + $this->assertSame([$node3, $node2, $mockNode], $document->getNodes()); + } +} diff --git a/tests/unit/Node/EmojiTest.php b/tests/unit/Node/EmojiTest.php new file mode 100644 index 0000000..0ed890b --- /dev/null +++ b/tests/unit/Node/EmojiTest.php @@ -0,0 +1,94 @@ +assertEmojiData($data, $expectedData); + } + + /** + * @param mixed[] $data + * @param mixed[] $expectedData + */ + public function assertEmojiData(array $data, array $expectedData): void + { + $datasetEmoji = new DatasetEmoji($data); + + $parsedValue = (string) $data['emoji']; + $emoji = new Emoji(EmojiLexer::T_UNICODE, $parsedValue, $datasetEmoji); + + $this->assertSame(EmojiLexer::T_UNICODE, $emoji->getParsedType()); + $this->assertSame($parsedValue, $emoji->getParsedValue()); + + // Test invalid skin tone. + $this->assertNull($emoji->getSkin(-1)); + + $this->assertSame(\get_class($datasetEmoji), DatasetEmoji::class); + $this->assertSame(\get_class($emoji), Emoji::class); + + foreach ($expectedData as $property => $expected) { + + /** @var array> $skinData */ + $skinData = isset($data['skins']) ? (array) $data['skins'] : []; + switch ($property) { + case 'skin': + if (isset($skinData) && \count($skinData) > 0) { + $expectedTone = (int) $expectedData; + + /** @var ?Emoji $expectedToneEmoji */ + $expectedToneEmoji = null; + if (isset($skinData[$expectedTone - 1])) { + /** @var mixed[] $skin */ + $skin = $skinData[$expectedTone - 1]; + $expectedToneEmoji = new Emoji(EmojiLexer::T_UNICODE, (string) $skin['emoji'], new DatasetEmoji($skin)); + } + + $actualToneEmoji = $emoji->getSkin($expectedTone); + if ($expectedToneEmoji === null) { + $this->assertNull($actualToneEmoji); + } else { + $this->assertSame(\get_class($actualToneEmoji), Emoji::class, $property); + $this->assertEquals($expectedToneEmoji->getArrayCopy(), $actualToneEmoji->getArrayCopy(), $property); + } + } else { + $this->assertSame($expected, $emoji->$property, $property); + } + + break; + + case 'skins': + $actualSkins = \array_map('iterator_to_array', $emoji->skins->getArrayCopy()); + + if (isset($expectedData['skins']) && \count($expectedData['skins']) > 0) { + $expectedSkins = isset($expectedData['skins']) ? (array) $expectedData['skins'] : []; + foreach ($expectedSkins as $expectedSkinData) { + $this->assertEmojiData(\array_shift($skinData), $expectedSkinData); + } + } else { + $this->assertSame($expected, $actualSkins, $property); + } + + break; + + default: + $this->assertSame($expected, $emoji->$property, $property); + } + } + } +} diff --git a/tests/unit/Node/ImageTest.php b/tests/unit/Node/ImageTest.php new file mode 100644 index 0000000..69e652a --- /dev/null +++ b/tests/unit/Node/ImageTest.php @@ -0,0 +1,68 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Node; + +use PHPUnit\Framework\TestCase; +use UnicornFail\Emoji\Dataset\Emoji as DatasetEmoji; +use UnicornFail\Emoji\Lexer\EmojiLexer; +use UnicornFail\Emoji\Node\Emoji; +use UnicornFail\Emoji\Node\Image; +use UnicornFail\Emoji\Tests\Unit\Dataset\EmojiTest; + +class ImageTest extends TestCase +{ + /** + * Tests the URL constructor parameter and getUrl() method + */ + public function testConstructorAndGetUrl(): void + { + $datasetEmoji = new DatasetEmoji(EmojiTest::GRINNING_FACE); + $value = (string) $datasetEmoji->shortcode; + $emoji = new Emoji(EmojiLexer::T_SHORTCODE, $value, $datasetEmoji); + $url = 'https://www.example.com/foo'; + + $element = $this->getMockBuilder(Image::class) + ->setConstructorArgs([$value, $emoji, $url]) + ->getMockForAbstractClass(); + \assert($element instanceof Image); + + $this->assertSame($emoji, $element->getEmoji()); + $this->assertEquals($url, $element->getUrl()); + } + + /** + * Tests the setUrl() method + */ + public function testSetUrl(): void + { + $datasetEmoji = new DatasetEmoji(EmojiTest::GRINNING_FACE); + $value = (string) $datasetEmoji->shortcode; + $emoji = new Emoji(EmojiLexer::T_SHORTCODE, $value, $datasetEmoji); + $url1 = 'https://www.example.com/foo'; + $url2 = 'https://www.example.com/bar'; + + $element = $this->getMockBuilder(Image::class) + ->setConstructorArgs([$value, $emoji, $url1]) + ->getMockForAbstractClass(); + \assert($element instanceof Image); + + $element->setUrl($url2); + + $this->assertSame($emoji, $element->getEmoji()); + $this->assertEquals($url2, $element->getUrl()); + } +} diff --git a/tests/unit/Node/Node1.php b/tests/unit/Node/Node1.php new file mode 100644 index 0000000..e7fc7e2 --- /dev/null +++ b/tests/unit/Node/Node1.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Node; + +use UnicornFail\Emoji\Node\Node; + +class Node1 extends Node +{ +} diff --git a/tests/unit/Node/Node2.php b/tests/unit/Node/Node2.php new file mode 100644 index 0000000..2f1c56e --- /dev/null +++ b/tests/unit/Node/Node2.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Node; + +class Node2 extends Node1 +{ +} diff --git a/tests/unit/Node/Node3.php b/tests/unit/Node/Node3.php new file mode 100644 index 0000000..a40a8f6 --- /dev/null +++ b/tests/unit/Node/Node3.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Node; + +class Node3 extends Node2 +{ +} diff --git a/tests/unit/Node/NodeTest.php b/tests/unit/Node/NodeTest.php new file mode 100644 index 0000000..9a9afba --- /dev/null +++ b/tests/unit/Node/NodeTest.php @@ -0,0 +1,89 @@ +assertSame('foo', $node1->getContent()); + $this->assertSame('foo', (string) $node1); + } + + public function testClone(): void + { + $document = new Document(); + $node1 = new Node1(); + + $document->appendNode($node1); + + $this->assertSame($document, $node1->getDocument()); + + // Ensure cloned nodes aren't attached to the document. + $clone = clone $node1; + $this->assertNull($clone->getDocument()); + $this->assertNotFalse(\array_search($node1, $document->getNodes(), true)); + $this->assertFalse(\array_search($clone, $document->getNodes(), true)); + } + + public function testData(): void + { + $node1 = new Node1(); + + $this->assertFalse($node1->has('foo')); + $this->assertFalse($node1->get('foo', false)); + + $node1->set('foo', 'bar'); + $this->assertTrue($node1->has('foo')); + $this->assertSame('bar', $node1->get('foo')); + } + + public function testAttributes(): void + { + $node1 = new Node1(); + + $this->assertFalse($node1->hasAttribute('foo')); + $this->assertFalse($node1->getAttribute('foo', false)); + + $node1->setAttribute('foo', 'bar'); + $this->assertTrue($node1->hasAttribute('foo')); + $this->assertSame('bar', $node1->getAttribute('foo')); + $this->assertSame(['foo' => 'bar'], $node1->getAttributes()->export()); + + $node1->setAttributes(['baz' => 'qux']); + $node1->addClass('foo', 'bar', 'baz'); + $this->assertSame([ + 'baz' => 'qux', + 'class' => 'foo bar baz', + ], $node1->getAttributes()->export()); + } + + public function testReplaceWith(): void + { + $document = new Document(); + $node1 = new Node1(); + $node2 = new Node2(); + + $document->appendNode($node1); + + $this->assertSame($document, $node1->getDocument()); + $this->assertNotFalse(\array_search($node1, $document->getNodes(), true)); + + $node1->replaceWith($node2); + + // Ensure first node is not attached to the document. + $this->assertNull($node1->getDocument()); + $this->assertFalse(\array_search($node1, $document->getNodes(), true)); + + // Ensure second node is attached to the document. + $this->assertSame($document, $node2->getDocument()); + $this->assertNotFalse(\array_search($node2, $document->getNodes(), true)); + } +} diff --git a/tests/unit/Parser/EmojiParserTest.php b/tests/unit/Parser/EmojiParserTest.php new file mode 100644 index 0000000..9789939 --- /dev/null +++ b/tests/unit/Parser/EmojiParserTest.php @@ -0,0 +1,56 @@ +environment = Environment::create(); + } + + public function testCustomLexer(): void + { + $lexer = $this->createMock(EmojiLexer::class); + $parser = new EmojiParser($this->environment, $lexer); + $this->assertSame($lexer, $parser->getLexer()); + } + + public function testInvalidContent(): void + { + $parser = new EmojiParser($this->environment); + + $this->expectException(UnexpectedEncodingException::class); + + $parser->parse(\chr(250)); + } + + public function testEmptyString(): void + { + $parser = new EmojiParser($this->environment); + + $document = $parser->parse(''); + + $this->assertCount(0, $document->getNodes()); + } + + public function testInvalidEmoji(): void + { + $parser = new EmojiParser($this->environment); + + $document = $parser->parse(':foo-bar-baz: '); + + $this->assertCount(1, $document->getNodes()); + } +} diff --git a/tests/unit/Renderer/DocumentRendererTest.php b/tests/unit/Renderer/DocumentRendererTest.php new file mode 100644 index 0000000..ee71ec5 --- /dev/null +++ b/tests/unit/Renderer/DocumentRendererTest.php @@ -0,0 +1,29 @@ +createMock(Node::class); + $document->appendNode($node); + + $this->expectException(RenderNodeException::class); + $this->expectExceptionMessage(\sprintf('Unable to find corresponding renderer for node type %s', \get_class($node))); + + $renderer->renderDocument($document); + } +} diff --git a/tests/unit/Renderer/EmojiRendererTest.php b/tests/unit/Renderer/EmojiRendererTest.php new file mode 100644 index 0000000..3d0fc96 --- /dev/null +++ b/tests/unit/Renderer/EmojiRendererTest.php @@ -0,0 +1,23 @@ +createMock(Node::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Incompatible node type: ' . \get_class($node)); + + $renderer->render($node); + } +} diff --git a/tests/unit/Renderer/ImageRendererTest.php b/tests/unit/Renderer/ImageRendererTest.php new file mode 100644 index 0000000..fdba71e --- /dev/null +++ b/tests/unit/Renderer/ImageRendererTest.php @@ -0,0 +1,64 @@ +createMock(Node::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Incompatible node type: ' . \get_class($node)); + + $renderer->render($node); + } + + public function testAllowUnsafeLinks(): void + { + $config = Environment::create()->getConfiguration(); + + $url = 'javascript:alert("foobar")'; + + $image = new Image('foo', new Emoji(0, 'foo', new DatasetEmoji([])), $url); + + $renderer = new ImageRenderer(); + $renderer->setConfiguration($config); + + /** @var HtmlElement $img */ + $img = $renderer->render($image); + + $this->assertSame($url, (string) $img->getAttribute('src')); + } + + public function testNotAllowUnsafeLinks(): void + { + $config = Environment::create([ + 'allow_unsafe_links' => false, + ])->getConfiguration(); + + $url = 'javascript:alert("foobar")'; + + $image = new Image('foo', new Emoji(0, 'foo', new DatasetEmoji([])), $url); + + $renderer = new ImageRenderer(); + $renderer->setConfiguration($config); + + /** @var HtmlElement $img */ + $img = $renderer->render($image); + + $this->assertSame('', (string) $img->getAttribute('src')); + } +} diff --git a/tests/unit/Renderer/TextRendererTest.php b/tests/unit/Renderer/TextRendererTest.php new file mode 100644 index 0000000..c0a6c6a --- /dev/null +++ b/tests/unit/Renderer/TextRendererTest.php @@ -0,0 +1,23 @@ +createMock(Node::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Incompatible node type: ' . \get_class($node)); + + $renderer->render($node); + } +} diff --git a/tests/unit/Util/ArrayCollectionTest.php b/tests/unit/Util/ArrayCollectionTest.php new file mode 100644 index 0000000..481c266 --- /dev/null +++ b/tests/unit/Util/ArrayCollectionTest.php @@ -0,0 +1,184 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Util; + +use PHPUnit\Framework\TestCase; +use UnicornFail\Emoji\Util\ArrayCollection; + +class ArrayCollectionTest extends TestCase +{ + public function testConstructorAndToArray(): void + { + $collection = new ArrayCollection(); + $this->assertEquals([], $collection->toArray()); + + $array = []; + $collection = new ArrayCollection($array); + $this->assertEquals($array, $collection->toArray()); + + $array = ['foo', 'bar']; + $collection = new ArrayCollection($array); + $this->assertEquals($array, $collection->toArray()); + } + + public function testFirst(): void + { + $collection = new ArrayCollection(['foo', 'bar']); + $this->assertEquals('foo', $collection->first()); + } + + public function testLast(): void + { + $collection = new ArrayCollection(['foo', 'bar']); + $this->assertEquals('bar', $collection->last()); + } + + public function testGetIterator(): void + { + $array = ['foo', 'bar']; + $collection = new ArrayCollection($array); + $iterator = $collection->getIterator(); + + $this->assertTrue($iterator instanceof \ArrayIterator); + $this->assertEquals($array, $iterator->getArrayCopy()); + } + + public function testOffsetExists(): void + { + $collection = new ArrayCollection(['foo', 2 => 'bar', 3, null]); + + $this->assertTrue($collection->offsetExists(0)); + $this->assertTrue($collection->offsetExists(2)); + $this->assertTrue($collection->offsetExists(3)); + $this->assertTrue($collection->offsetExists(4)); + $this->assertTrue(isset($collection[0])); + $this->assertTrue(isset($collection[2])); + $this->assertTrue(isset($collection[3])); + $this->assertTrue(isset($collection[4])); + + $this->assertFalse($collection->offsetExists(1)); + $this->assertFalse(isset($collection[1])); + } + + public function testOffsetGet(): void + { + $collection = new ArrayCollection(['foo', 2 => 'bar']); + + $this->assertEquals('foo', $collection[0]); + $this->assertEquals('foo', $collection->offsetGet(0)); + $this->assertEquals('bar', $collection[2]); + $this->assertEquals('bar', $collection->offsetGet(2)); + } + + public function testOffsetSet(): void + { + $collection = new ArrayCollection(); + + $collection[] = 'foo'; + $this->assertEquals(['foo'], $collection->toArray()); + + $collection[] = 'bar'; + $this->assertEquals(['foo', 'bar'], $collection->toArray()); + + $collection = new ArrayCollection(['foo']); + + $collection[42] = true; + $this->assertEquals(['foo', 42 => true], $collection->toArray()); + + $collection[42] = false; + $this->assertEquals(['foo', 42 => false], $collection->toArray()); + } + + public function testOffsetUnset(): void + { + $collection = new ArrayCollection(['foo', 'bar', 'baz']); + + unset($collection[0]); + $this->assertEquals([1 => 'bar', 2 => 'baz'], $collection->toArray()); + + unset($collection[9999]); + $this->assertEquals([1 => 'bar', 2 => 'baz'], $collection->toArray()); + + unset($collection[1]); + $this->assertEquals([2 => 'baz'], $collection->toArray()); + + unset($collection[2]); + $this->assertEquals([], $collection->toArray()); + } + + public function testOffsetUnsetWithNulls(): void + { + $collection = new ArrayCollection([2 => null]); + + unset($collection[99999]); + $this->assertEquals([2 => null], $collection->toArray()); + + unset($collection[2]); + $this->assertEquals([], $collection->toArray()); + } + + public function testSlice(): void + { + $collection = new ArrayCollection(['foo', 'bar', 'baz', 42 => 'ftw']); + + $this->assertEquals(['foo', 'bar', 'baz', 42 => 'ftw'], $collection->slice(0)); + $this->assertEquals(['foo', 'bar', 'baz', 42 => 'ftw'], $collection->slice(0, null)); + $this->assertEquals(['foo', 'bar', 'baz', 42 => 'ftw'], $collection->slice(0, 99)); + $this->assertEquals(['foo', 'bar', 'baz', 42 => 'ftw'], $collection->slice(0, 4)); + $this->assertEquals(['foo', 'bar', 'baz'], $collection->slice(0, 3)); + $this->assertEquals(['foo', 'bar'], $collection->slice(0, 2)); + $this->assertEquals(['foo'], $collection->slice(0, 1)); + $this->assertEquals([], $collection->slice(0, 0)); + + $this->assertEquals([1 => 'bar', 2 => 'baz', 42 => 'ftw'], $collection->slice(1)); + $this->assertEquals([1 => 'bar', 2 => 'baz', 42 => 'ftw'], $collection->slice(1, null)); + $this->assertEquals([1 => 'bar', 2 => 'baz', 42 => 'ftw'], $collection->slice(1, 99)); + $this->assertEquals([1 => 'bar', 2 => 'baz', 42 => 'ftw'], $collection->slice(1, 3)); + $this->assertEquals([1 => 'bar', 2 => 'baz'], $collection->slice(1, 2)); + $this->assertEquals([1 => 'bar'], $collection->slice(1, 1)); + $this->assertEquals([], $collection->slice(1, 0)); + + $this->assertEquals([2 => 'baz', 42 => 'ftw'], $collection->slice(2)); + $this->assertEquals([2 => 'baz', 42 => 'ftw'], $collection->slice(2, null)); + $this->assertEquals([2 => 'baz', 42 => 'ftw'], $collection->slice(2, 99)); + $this->assertEquals([2 => 'baz', 42 => 'ftw'], $collection->slice(2, 2)); + $this->assertEquals([2 => 'baz'], $collection->slice(2, 1)); + $this->assertEquals([], $collection->slice(2, 0)); + + $this->assertEquals([42 => 'ftw'], $collection->slice(3)); + $this->assertEquals([42 => 'ftw'], $collection->slice(3, null)); + $this->assertEquals([42 => 'ftw'], $collection->slice(3, 99)); + $this->assertEquals([42 => 'ftw'], $collection->slice(3, 1)); + $this->assertEquals([], $collection->slice(3, 0)); + + $this->assertEquals([], $collection->slice(4)); + $this->assertEquals([], $collection->slice(99)); + $this->assertEquals([], $collection->slice(99, 99)); + } + + public function testToArray(): void + { + $collection = new ArrayCollection(); + $this->assertEquals([], $collection->toArray()); + + $collection = new ArrayCollection([]); + $this->assertEquals([], $collection->toArray()); + + $collection = new ArrayCollection([1]); + $this->assertEquals([1], $collection->toArray()); + + $collection = new ArrayCollection([2 => 1, 'foo']); + $this->assertEquals([2 => 1, 'foo'], $collection->toArray()); + } +} diff --git a/tests/unit/Util/HtmlElementTest.php b/tests/unit/Util/HtmlElementTest.php new file mode 100644 index 0000000..f60d2eb --- /dev/null +++ b/tests/unit/Util/HtmlElementTest.php @@ -0,0 +1,204 @@ + + * + * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) + * - (c) John MacFarlane + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace UnicornFail\Emoji\Tests\Unit\Util; + +use PHPUnit\Framework\TestCase; +use UnicornFail\Emoji\Util\HtmlElement; + +class HtmlElementTest extends TestCase +{ + public function testConstructorOneArgument(): void + { + $p = new HtmlElement('p'); + $this->assertEquals('p', $p->getTagName()); + $this->assertEmpty($p->getAllAttributes()); + $this->assertEmpty($p->getContents()); + } + + public function testConstructorTwoArguments(): void + { + $img = new HtmlElement('img', ['src' => 'foo.jpg']); + $this->assertEquals('img', $img->getTagName()); + $this->assertCount(1, $img->getAllAttributes()); + $this->assertEquals('foo.jpg', $img->getAttribute('src')); + $this->assertEmpty($img->getContents()); + } + + public function testConstructorThreeArguments(): void + { + $li = new HtmlElement('li', ['class' => 'odd'], 'Foo'); + $this->assertEquals('li', $li->getTagName()); + $this->assertCount(1, $li->getAllAttributes()); + $this->assertEquals('odd', $li->getAttribute('class')); + $this->assertEquals('Foo', $li->getContents()); + } + + public function testNonSelfClosingElement(): void + { + $p = new HtmlElement('p', [], '', false); + + $this->assertEquals('

', (string) $p); + } + + public function testSelfClosingElement(): void + { + $hr = new HtmlElement('hr', [], '', true); + + $this->assertEquals('
', (string) $hr); + } + + public function testGetSetExistingAttribute(): void + { + $p = new HtmlElement('p', ['class' => 'foo']); + $this->assertCount(1, $p->getAllAttributes()); + $this->assertEquals('foo', $p->getAttribute('class')); + + $p->setAttribute('class', 'bar'); + $this->assertCount(1, $p->getAllAttributes()); + $this->assertEquals('bar', $p->getAttribute('class')); + } + + public function testGetSetNonExistingAttribute(): void + { + $p = new HtmlElement('p', ['class' => 'foo']); + $this->assertCount(1, $p->getAllAttributes()); + $this->assertNull($p->getAttribute('id')); + + $p->setAttribute('id', 'bar'); + $this->assertCount(2, $p->getAllAttributes()); + $this->assertEquals('bar', $p->getAttribute('id')); + $this->assertEquals('foo', $p->getAttribute('class')); + } + + public function testGetSetAttributeWithStringAndArrayValues(): void + { + $p = new HtmlElement('p', ['class' => ['foo', 'bar']]); + $this->assertCount(1, $p->getAllAttributes()); + $this->assertSame('foo bar', $p->getAttribute('class')); + + $p->addClass('baz'); + $this->assertSame('foo bar baz', $p->getAttribute('class')); + + $p->setAttribute('class', 'baz'); + $this->assertSame('baz', $p->getAttribute('class')); + + $p->setAttribute('class', ['foo', 'bar', 'baz']); + $this->assertSame('foo bar baz', $p->getAttribute('class')); + + $p->setAttribute('class', 'foo bar'); + $this->assertSame('foo bar', $p->getAttribute('class')); + } + + public function testAttributesWithArrayValues(): void + { + // Classes have duplicate values removed (array or string). + $p = new HtmlElement('p', ['class' => ['a', 'b', 'a']]); + $this->assertCount(1, $p->getAllAttributes()); + $this->assertSame('a b', $p->getAttribute('class')); + $this->assertSame('

', $p->__toString()); + + $p->setAttribute('class', ['foo', 'bar__baz', 'foo']); + $this->assertCount(1, $p->getAllAttributes()); + $this->assertSame('foo bar__baz', $p->getAttribute('class')); + $this->assertSame('

', $p->__toString()); + + $p->setAttribute('class', 'x y z x a'); + $this->assertCount(1, $p->getAllAttributes()); + $this->assertSame('x y z a', $p->getAttribute('class')); + $this->assertSame('

', $p->__toString()); + + // Normal attribute values only have duplicates removed if an array is passed. + $p = new HtmlElement('p', ['data-attribute' => ['a', 'b', 'a']]); + $this->assertCount(1, $p->getAllAttributes()); + $this->assertSame('a b', $p->getAttribute('data-attribute')); + $this->assertSame('

', $p->__toString()); + + $p->setAttribute('data-attribute', ['foo', 'bar', 'foo']); + $this->assertCount(1, $p->getAllAttributes()); + $this->assertSame('foo bar', $p->getAttribute('data-attribute')); + $this->assertSame('

', $p->__toString()); + + $p->setAttribute('data-attribute', 'x y z x a'); + $this->assertCount(1, $p->getAllAttributes()); + $this->assertSame('x y z x a', $p->getAttribute('data-attribute')); + $this->assertSame('

', $p->__toString()); + } + + public function testAttributesWithBooleanTrueValues(): void + { + $checkbox = new HtmlElement('input', ['type' => 'checkbox', 'checked' => true], '', true); + $this->assertSame('', $checkbox->__toString()); + + $checkbox->setAttribute('checked', false); + $this->assertSame('', $checkbox->__toString()); + + $checkbox->setAttribute('checked', true); + $this->assertSame('', $checkbox->__toString()); + } + + public function testToString(): void + { + $img = new HtmlElement('img', [], '', true); + $p = new HtmlElement('p'); + $div = new HtmlElement('div'); + + $div->setContents($p); + $this->assertEquals('

', $div->getContents(true)); + + $div->setContents([$p, $img]); + $this->assertIsString($div->getContents(true)); + $this->assertEquals('

', $div->getContents(true)); + + $this->assertEquals('

', $div->__toString()); + } + + public function testToStringWithUnescapedAttribute(): void + { + $element = new HtmlElement('p', ['id' => 'foo', 'data-attribute' => 'test" onclick="javascript:doBadThings();'], 'click me'); + $this->assertEquals('

click me

', $element->__toString()); + + // Ensure class sanitizes everything. + $element = new HtmlElement('p', ['id' => 'foo', 'class' => 'test" onclick="javascript:doBadThings();'], 'click me'); + $this->assertEquals('

click me

', $element->__toString()); + } + + public function testNullContentConstructor(): void + { + $img = new HtmlElement('img', [], null); + $this->assertTrue($img->getContents(false) === ''); + } + + public function testNullContentSetter(): void + { + $img = new HtmlElement('img'); + $img->setContents(null); + $this->assertTrue($img->getContents(false) === ''); + } + + /** + * See https://github.com/thephpleague/commonmark/issues/376 + */ + public function testRegressionWith0NotBeingRendered(): void + { + $element = new HtmlElement('em'); + $element->setContents('0'); + $this->assertSame('0', $element->getContents()); + + $element = new HtmlElement('em', [], '0'); + $this->assertSame('0', $element->getContents()); + } +} diff --git a/tests/unit/Util/ImmutableArrayIteratorTest.php b/tests/unit/Util/ImmutableArrayIteratorTest.php index b6e74fc..a7e71b0 100644 --- a/tests/unit/Util/ImmutableArrayIteratorTest.php +++ b/tests/unit/Util/ImmutableArrayIteratorTest.php @@ -16,7 +16,7 @@ public function providerImmutabilityMethods(): array { $methods = [['__set'], ['__unset'], ['append'], ['offsetSet'], ['offsetUnset'], ['setFlags']]; - return \array_combine(\array_map('current', $methods), $methods); + return (array) \array_combine(\array_map('current', $methods), $methods); } public function testGet(): void